blob: 4a4d073f0f090b2845ed1011d919188a0681aab5 [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 'dart:core' as core;
import 'dart:core';
import 'package:_fe_analyzer_shared/src/flow_analysis/flow_analysis.dart';
import 'package:_fe_analyzer_shared/src/flow_analysis/flow_analysis_operations.dart';
import 'package:_fe_analyzer_shared/src/flow_analysis/flow_link.dart';
import 'package:_fe_analyzer_shared/src/type_inference/assigned_variables.dart';
import 'package:_fe_analyzer_shared/src/types/shared_type.dart';
import 'package:test/test.dart';
import '../mini_ast.dart';
import '../mini_types.dart';
import 'flow_analysis_mini_ast.dart';
main() {
late FlowAnalysisTestHarness h;
setUp(() {
h = FlowAnalysisTestHarness();
});
group('API', () {
test('asExpression_end promotes variables', () {
var x = Var('x');
late SsaNode<SharedTypeView<Type>> ssaBeforePromotion;
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
getSsaNodes((nodes) => ssaBeforePromotion = nodes[x]!),
x.as_('int'),
checkPromoted(x, 'int'),
getSsaNodes((nodes) => expect(nodes[x], same(ssaBeforePromotion))),
]);
});
test('asExpression_end handles other expressions', () {
h.run([
expr('Object').as_('int'),
]);
});
test("asExpression_end() sets reachability for Never", () {
// Note: this is handled by the general mechanism that marks control flow
// as reachable after any expression with static type `Never`. This is
// implemented in the flow analysis client, but we test it here anyway as
// a validation of the "mini AST" logic.
h.run([
checkReachable(true),
expr('int').as_('Never'),
checkReachable(false),
]);
});
test('assert_afterCondition promotes', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
assert_(
x.eq(nullLiteral), second(checkPromoted(x, 'int'), expr('String'))),
]);
});
test('assert_end joins previous and ifTrue states', () {
var x = Var('x');
var y = Var('y');
var z = Var('z');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
declare(y, type: 'int?', initializer: expr('int?')),
declare(z, type: 'int?', initializer: expr('int?')),
x.as_('int'),
z.as_('int'),
assert_(second(
listLiteral(elementType: 'dynamic', [
x.write(expr('int?')),
z.write(expr('int?')),
]),
expr('bool'))
.and(x.notEq(nullLiteral).and(y.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 x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
x.notEq(nullLiteral).conditional(
second(checkPromoted(x, 'int'), expr('int')),
second(checkNotPromoted(x), expr('int'))),
checkNotPromoted(x),
]);
});
test('conditional_elseBegin promotes false branch', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
x.eq(nullLiteral).conditional(second(checkNotPromoted(x), expr('Null')),
second(checkPromoted(x, 'int'), expr('Null'))),
checkNotPromoted(x),
]);
});
test('conditional_end keeps promotions common to true and false branches',
() {
var x = Var('x');
var y = Var('y');
var z = Var('z');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
declare(y, type: 'int?', initializer: expr('int?')),
declare(z, type: 'int?', initializer: expr('int?')),
expr('bool').conditional(
second(
listLiteral(elementType: 'dynamic', [
x.as_('int'),
y.as_('int'),
]),
expr('Null')),
second(
listLiteral(elementType: 'dynamic', [
x.as_('int'),
z.as_('int'),
]),
expr('Null'))),
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 x = Var('x');
var y = Var('y');
var z = Var('z');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
declare(y, type: 'int?', initializer: expr('int?')),
declare(z, type: 'int?', initializer: expr('int?')),
if_(
expr('bool').conditional(
x.notEq(nullLiteral).and(y.notEq(nullLiteral)),
x.notEq(nullLiteral).and(z.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 x = Var('x');
var y = Var('y');
var z = Var('z');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
declare(y, type: 'int?', initializer: expr('int?')),
declare(z, type: 'int?', initializer: expr('int?')),
if_(
expr('bool').conditional(x.eq(nullLiteral).or(y.eq(nullLiteral)),
x.eq(nullLiteral).or(z.eq(nullLiteral))),
[],
[
checkPromoted(x, 'int'),
checkNotPromoted(y),
checkNotPromoted(z),
]),
]);
});
test('declare() sets Ssa', () {
var x = Var('x');
h.run([
declare(x, type: 'Object'),
getSsaNodes((nodes) {
expect(nodes[x], isNotNull);
}),
]);
});
test('equalityOp(x != null) promotes true branch', () {
var x = Var('x');
late SsaNode<SharedTypeView<Type>> ssaBeforePromotion;
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
getSsaNodes((nodes) => ssaBeforePromotion = nodes[x]!),
if_(x.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 x = Var('x');
h.run([
declare(x, type: 'int', initializer: expr('int')),
if_(x.notEq(nullLiteral), [
checkReachable(true),
checkNotPromoted(x),
], [
checkReachable(true),
checkNotPromoted(x),
])
]);
});
test('equalityOp(<expr> == <expr>) has no special effect', () {
h.run([
if_(expr('int?').eq(expr('int?')), [
checkReachable(true),
], [
checkReachable(true),
]),
]);
});
test('equalityOp(<expr> != <expr>) has no special effect', () {
h.run([
if_(expr('int?').notEq(expr('int?')), [
checkReachable(true),
], [
checkReachable(true),
]),
]);
});
test('equalityOp(x != <null expr>) does not promote', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
if_(x.notEq(expr('Null')), [
checkNotPromoted(x),
], [
checkNotPromoted(x),
]),
]);
});
test('equalityOp(x == null) promotes false branch', () {
var x = Var('x');
late SsaNode<SharedTypeView<Type>> ssaBeforePromotion;
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
getSsaNodes((nodes) => ssaBeforePromotion = nodes[x]!),
if_(x.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 x = Var('x');
h.run([
declare(x, type: 'int', initializer: expr('int')),
if_(x.eq(nullLiteral), [
checkReachable(true),
checkNotPromoted(x),
], [
checkReachable(true),
checkNotPromoted(x),
])
]);
});
test('equalityOp(null != x) promotes true branch', () {
var x = Var('x');
late SsaNode<SharedTypeView<Type>> ssaBeforePromotion;
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
getSsaNodes((nodes) => ssaBeforePromotion = nodes[x]!),
if_(nullLiteral.notEq(x), [
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 x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
if_(expr('Null').notEq(x), [
checkNotPromoted(x),
], [
checkNotPromoted(x),
]),
]);
});
test('equalityOp(null == x) promotes false branch', () {
var x = Var('x');
late SsaNode<SharedTypeView<Type>> ssaBeforePromotion;
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
getSsaNodes((nodes) => ssaBeforePromotion = nodes[x]!),
if_(nullLiteral.eq(x), [
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', () {
h.run([
if_(expr('Null').eq(expr('Null')), [
checkReachable(true),
], [
checkReachable(false),
]),
]);
});
test('equalityOp(null != null) equivalent to false', () {
h.run([
if_(expr('Null').notEq(expr('Null')), [
checkReachable(false),
], [
checkReachable(true),
]),
]);
});
test('equalityOp(null == non-null) is not equivalent to false', () {
h.run([
if_(expr('Null').eq(expr('int')), [
checkReachable(true),
], [
checkReachable(true),
]),
]);
});
test('equalityOp(null != non-null) is not equivalent to true', () {
h.run([
if_(expr('Null').notEq(expr('int')), [
checkReachable(true),
], [
checkReachable(true),
]),
]);
});
test('equalityOp(non-null == null) is not equivalent to false', () {
h.run([
if_(expr('int').eq(expr('Null')), [
checkReachable(true),
], [
checkReachable(true),
]),
]);
});
test('equalityOp(non-null != null) is not equivalent to true', () {
h.run([
if_(expr('int').notEq(expr('Null')), [
checkReachable(true),
], [
checkReachable(true),
]),
]);
});
test('conditionEqNull() does not promote write-captured vars', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
if_(x.notEq(nullLiteral), [
checkPromoted(x, 'int'),
]),
localFunction([
x.write(expr('int?')),
]),
if_(x.notEq(nullLiteral), [
checkNotPromoted(x),
]),
]);
});
test('declare(initialized: false) assigns new SSA ids', () {
var x = Var('x');
var y = Var('y');
h.run([
declare(x, type: 'int?'),
declare(y, type: 'int?'),
getSsaNodes((nodes) => expect(nodes[y], isNot(nodes[x]))),
]);
});
test('declare(initialized: true) assigns new SSA ids', () {
var x = Var('x');
var y = Var('y');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
declare(y, type: 'int?', initializer: expr('int?')),
getSsaNodes((nodes) => expect(nodes[y], isNot(nodes[x]))),
]);
});
test('doStatement_bodyBegin() un-promotes', () {
var x = Var('x');
late SsaNode<SharedTypeView<Type>> ssaBeforeLoop;
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
x.as_('int'),
checkPromoted(x, 'int'),
getSsaNodes((nodes) => ssaBeforeLoop = nodes[x]!),
do_([
getSsaNodes((nodes) => expect(nodes[x], isNot(ssaBeforeLoop))),
checkNotPromoted(x),
x.write(expr('Null')),
], expr('bool')),
]);
});
test('doStatement_bodyBegin() handles write captures in the loop', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
do_([
x.as_('int'),
// 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?')),
]),
], expr('bool')),
]);
});
test('doStatement_conditionBegin() joins continue state', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
do_(
[
if_(x.notEq(nullLiteral), [
continue_(),
]),
return_(),
checkReachable(false),
checkNotPromoted(x),
],
second(
listLiteral(elementType: 'dynamic', [
checkReachable(true),
checkPromoted(x, 'int'),
]),
expr('bool'))),
]);
});
test('doStatement_end() promotes', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
do_([],
second(checkNotPromoted(x), expr('bool')).or(x.eq(nullLiteral))),
checkPromoted(x, 'int'),
]);
});
test('equalityOp_end on property get preserves target variable', () {
// This is a regression test for a mistake made during the implementation
// of "why not promoted" functionality: when storing information about an
// attempt to promote a field (e.g. `x.y != null`) we need to make sure we
// don't wipe out information about the target variable (`x`).
h.addMember('C', 'y', 'Object?');
var x = Var('x');
h.run([
declare(x, type: 'C', initializer: expr('C')),
checkAssigned(x, true),
if_(x.property('y').notEq(nullLiteral), [
checkAssigned(x, true),
], [
checkAssigned(x, true),
]),
]);
});
test('equalityOp_end does not set reachability for `this`', () {
h.thisType = 'C';
h.addSuperInterfaces('C', (_) => [Type('Object')]);
h.run([
if_(this_.is_('Null'), [
if_(this_.eq(nullLiteral), [
checkReachable(true),
], [
checkReachable(true),
]),
]),
]);
});
group('equalityOp_end does not set reachability for property gets', () {
test('on a variable', () {
h.addMember('C', 'f', 'Object?');
var x = Var('x');
h.run([
declare(x, type: 'C', initializer: expr('C')),
if_(x.property('f').is_('Null'), [
if_(x.property('f').eq(nullLiteral), [
checkReachable(true),
], [
checkReachable(true),
]),
]),
]);
});
test('on an arbitrary expression', () {
h.addMember('C', 'f', 'Object?');
h.run([
if_(expr('C').property('f').is_('Null'), [
if_(expr('C').property('f').eq(nullLiteral), [
checkReachable(true),
], [
checkReachable(true),
]),
]),
]);
});
test('on explicit this', () {
h.thisType = 'C';
h.addMember('C', 'f', 'Object?');
h.run([
if_(this_.property('f').is_('Null'), [
if_(this_.property('f').eq(nullLiteral), [
checkReachable(true),
], [
checkReachable(true),
]),
]),
]);
});
test('on implicit this/super', () {
h.thisType = 'C';
h.addMember('C', 'f', 'Object?');
h.run([
if_(thisProperty('f').is_('Null'), [
if_(thisProperty('f').eq(nullLiteral), [
checkReachable(true),
], [
checkReachable(true),
]),
]),
]);
});
});
test('finish checks proper nesting', () {
var e = expr('Null');
var s = if_(e, []);
var flow =
FlowAnalysis<Node, Statement, Expression, Var, SharedTypeView<Type>>(
h.typeOperations, AssignedVariables<Node, Var>(),
respectImplicitlyTypedVarInitializers: true,
fieldPromotionEnabled: true);
flow.ifStatement_conditionBegin();
flow.ifStatement_thenBegin(e, s);
expect(() => flow.finish(), _asserts);
});
test('for_conditionBegin() un-promotes', () {
var x = Var('x');
late SsaNode<SharedTypeView<Type>> ssaBeforeLoop;
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
x.as_('int'),
checkPromoted(x, 'int'),
getSsaNodes((nodes) => ssaBeforeLoop = nodes[x]!),
for_(
null,
second(
listLiteral(elementType: 'dynamic', [
checkNotPromoted(x),
getSsaNodes(
(nodes) => expect(nodes[x], isNot(ssaBeforeLoop))),
]),
expr('bool')),
null,
[
x.write(expr('int?')),
]),
]);
});
test('for_conditionBegin() handles write captures in the loop', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
x.as_('int'),
checkPromoted(x, 'int'),
for_(
null,
second(
listLiteral(elementType: 'dynamic', [
x.as_('int'),
checkNotPromoted(x),
localFunction([
x.write(expr('int?')),
]),
]),
expr('bool')),
null,
[]),
]);
});
test('for_bodyBegin() handles empty condition', () {
h.run([
for_(null, null, second(checkReachable(true), expr('Null')), []),
checkReachable(false),
]);
});
test('for_bodyBegin() promotes', () {
var x = Var('x');
h.run([
for_(declare(x, type: 'int?', initializer: expr('int?')),
x.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 x = Var('x');
h.run([
for_(declare(x, type: 'int?', initializer: expr('int?')),
x.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 x = Var('x');
var y = Var('y');
var z = Var('z');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
declare(y, type: 'int?', initializer: expr('int?')),
declare(z, type: 'int?', initializer: expr('int?')),
for_(
null,
expr('bool'),
second(
listLiteral(elementType: 'dynamic', [
checkPromoted(x, 'int'),
checkNotPromoted(y),
checkNotPromoted(z),
]),
expr('Null')),
[
if_(expr('bool'), [
x.as_('int'),
y.as_('int'),
continue_(),
]),
x.as_('int'),
z.as_('int'),
]),
]);
});
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 x = Var('x');
var y = Var('y');
var z = Var('z');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
declare(y, type: 'int?', initializer: expr('int?')),
declare(z, type: 'int?', initializer: expr('int?')),
for_(null, x.eq(nullLiteral).or(z.eq(nullLiteral)), null, [
if_(expr('bool'), [
x.as_('int'),
y.as_('int'),
break_(),
]),
]),
checkPromoted(x, 'int'),
checkNotPromoted(y),
checkNotPromoted(z),
]);
});
test('for_end() with break updates Ssa of modified vars', () {
var x = Var('x');
var y = Var('y');
late SsaNode<SharedTypeView<Type>> xSsaInsideLoop;
late SsaNode<SharedTypeView<Type>> ySsaInsideLoop;
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
declare(y, type: 'int?', initializer: expr('int?')),
for_(null, expr('bool'), null, [
x.write(expr('int?')),
if_(expr('bool'), [break_()]),
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 x = Var('x');
var y = Var('y');
late SsaNode<SharedTypeView<Type>> xSsaInsideLoop;
late SsaNode<SharedTypeView<Type>> ySsaInsideLoop;
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
declare(y, type: 'int?', initializer: expr('int?')),
for_(null, expr('bool'), null, [
x.write(expr('int?')),
if_(expr('bool'), [break_()]),
if_(x.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 x = Var('x');
late SsaNode<SharedTypeView<Type>> ssaBeforeLoop;
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
x.as_('int'),
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?')),
]),
]);
});
test('forEach_bodyBegin() handles write captures in the loop', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
x.as_('int'),
checkPromoted(x, 'int'),
forEachWithNonVariable(expr('List<int?>'), [
x.as_('int'),
checkNotPromoted(x),
localFunction([
x.write(expr('int?')),
]),
]),
]);
});
test('forEach_bodyBegin() writes to loop variable', () {
var x = Var('x');
h.run([
declare(x, type: 'int?'),
checkAssigned(x, false),
forEachWithVariableSet(x, expr('List<int?>'), [
checkAssigned(x, true),
]),
checkAssigned(x, false),
]);
});
test('forEach_bodyBegin() does not write capture loop variable', () {
var x = Var('x');
h.run([
declare(x, type: 'int?'),
checkAssigned(x, false),
forEachWithVariableSet(x, expr('List<int?>'), [
checkAssigned(x, true),
if_(x.notEq(nullLiteral), [checkPromoted(x, 'int')]),
]),
checkAssigned(x, false),
]);
});
test('forEach_bodyBegin() pushes conservative join state', () {
var x = Var('x');
h.run([
declare(x, type: 'int'),
checkUnassigned(x, true),
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_(), x.write(expr('int')),
]),
// 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 x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
forEachWithNonVariable(expr('List<int?>'), [
x.as_('int'),
checkPromoted(x, 'int'),
]),
checkNotPromoted(x),
]);
});
test('functionExpression_begin() cancels promotions of self-captured vars',
() {
var x = Var('x');
var y = Var('y');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
declare(y, type: 'int?', initializer: expr('int?')),
x.as_('int'),
y.as_('int'),
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?')), x.as_('int'),
]),
// 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 x = Var('x');
var y = Var('y');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
declare(y, type: 'int?', initializer: expr('int?')),
x.as_('int'), y.as_('int'),
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.as_('int'),
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?')),
]),
// 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 x = Var('x');
var y = Var('y');
late SsaNode<SharedTypeView<Type>> ssaBeforeFunction;
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
declare(y, type: 'int?', initializer: expr('int?')),
x.as_('int'), y.as_('int'),
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.as_('int'),
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?')),
// x is unpromoted now.
checkNotPromoted(x), checkPromoted(y, 'int'),
]);
});
test('functionExpression_begin() preserves promotions of initialized vars',
() {
var x = Var('x');
var y = Var('y');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
declare(y, type: 'int?', initializer: expr('int?'), isLate: true),
x.as_('int'),
y.as_('int'),
checkPromoted(x, 'int'),
checkPromoted(y, 'int'),
localFunction([
// x and y remain promoted within the local function, because the
// assignment that happens implicitly as part of the initialization
// definitely happens before anything else, and hence the promotions
// are still valid whenever the local function executes.
checkPromoted(x, 'int'),
checkPromoted(y, 'int'),
]),
// x and y remain promoted after the local function too.
checkPromoted(x, 'int'),
checkPromoted(y, 'int'),
]);
});
test('functionExpression_begin() handles not-yet-seen variables', () {
var x = Var('x');
h.run([
localFunction([]),
// x is declared after the local function, so the local function
// cannot possibly write to x.
declare(x, type: 'int?', initializer: expr('int?')),
x.as_('int'),
checkPromoted(x, 'int'), x.write(expr('Null')),
]);
});
test('functionExpression_begin() handles not-yet-seen write-captured vars',
() {
var x = Var('x');
var y = Var('y');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
declare(y, type: 'int?', initializer: expr('int?')),
y.as_('int'),
getSsaNodes((nodes) => expect(nodes[x], isNotNull)),
localFunction([
getSsaNodes((nodes) => expect(nodes[x], isNot(nodes[y]))),
x.as_('int'),
// Promotion should not occur, because x might be write-captured by
// the time this code is reached.
checkNotPromoted(x),
]),
localFunction([
x.write(expr('Null')),
]),
]);
});
test(
'functionExpression_end does not propagate "definitely unassigned" '
'data', () {
var x = Var('x');
h.run([
declare(x, type: 'int'),
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')),
checkUnassigned(x, false),
]);
});
test('handleBreak handles deep nesting', () {
h.run([
while_(booleanLiteral(true), [
if_(expr('bool'), [
if_(expr('bool'), [
break_(),
]),
]),
return_(),
checkReachable(false),
]),
checkReachable(true),
]);
});
test('handleBreak handles mixed nesting', () {
h.run([
while_(booleanLiteral(true), [
if_(expr('bool'), [
if_(expr('bool'), [
break_(),
]),
break_(),
]),
break_(),
checkReachable(false),
]),
checkReachable(true),
]);
});
test('handleBreak handles null target', () {
h.run([
while_(booleanLiteral(true), [
checkReachable(true),
break_(Label.unbound()),
checkReachable(false),
]),
checkReachable(false),
]);
});
test('handleContinue handles deep nesting', () {
h.run([
do_([
if_(expr('bool'), [
if_(expr('bool'), [
continue_(),
]),
]),
return_(),
checkReachable(false),
], second(checkReachable(true), expr('bool')).or(booleanLiteral(true))),
checkReachable(false),
]);
});
test('handleContinue handles mixed nesting', () {
h.run([
do_([
if_(expr('bool'), [
if_(expr('bool'), [
continue_(),
]),
continue_(),
]),
continue_(),
checkReachable(false),
], second(checkReachable(true), expr('bool')).or(booleanLiteral(true))),
checkReachable(false),
]);
});
test('handleContinue handles null target', () {
h.run([
for_(null, booleanLiteral(true),
second(checkReachable(false), expr('Object?')), [
checkReachable(true),
continue_(Label.unbound()),
checkReachable(false),
]),
checkReachable(false),
]);
});
test('ifNullExpression allows ensure guarding', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
x
.ifNull(second(
listLiteral(elementType: 'dynamic', [
checkReachable(true),
x.write(expr('int')),
checkPromoted(x, 'int'),
]),
expr('int?')))
.thenStmt(block([
checkReachable(true),
checkPromoted(x, 'int'),
])),
]);
});
test('ifNullExpression allows promotion of tested var', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
x
.ifNull(second(
listLiteral(elementType: 'dynamic', [
checkReachable(true),
x.as_('int'),
checkPromoted(x, 'int'),
]),
expr('int?')))
.thenStmt(block([
checkReachable(true),
checkPromoted(x, 'int'),
])),
]);
});
test('ifNullExpression discards promotions unrelated to tested expr', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
expr('int?')
.ifNull(second(
listLiteral(elementType: 'dynamic', [
checkReachable(true),
x.as_('int'),
checkPromoted(x, 'int'),
]),
expr('int?')))
.thenStmt(block([
checkReachable(true),
checkNotPromoted(x),
])),
]);
});
test('ifNullExpression does not detect when RHS is unreachable', () {
h.run([
expr('int')
.ifNull(second(checkReachable(true), expr('int')))
.thenStmt(checkReachable(true)),
]);
});
test('ifNullExpression determines reachability correctly for `Null` type',
() {
h.run([
expr('Null')
.ifNull(second(checkReachable(true), expr('Null')))
.thenStmt(checkReachable(true)),
]);
});
test(
'ifNullExpression sets shortcut reachability correctly for `Null` type',
() {
h.run([
expr('Null')
.ifNull(second(checkReachable(true), throw_(expr('Object'))))
.thenStmt(checkReachable(false)),
]);
});
test(
'ifNullExpression sets shortcut reachability correctly for non-null '
'type', () {
h.run([
expr('Object')
.ifNull(second(checkReachable(true), throw_(expr('Object'))))
.thenStmt(checkReachable(true)),
]);
});
test('ifStatement with early exit promotes in unreachable code', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
return_(),
checkReachable(false),
if_(x.eq(nullLiteral), [
return_(),
]),
checkReachable(false),
checkPromoted(x, 'int'),
]);
});
test('ifStatement_end(false) keeps else branch if then branch exits', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
if_(x.eq(nullLiteral), [
return_(),
]),
checkPromoted(x, 'int'),
]);
});
test(
'ifStatement_end() discards non-matching expression info from joined '
'branches', () {
var w = Var('w');
var x = Var('x');
var y = Var('y');
var z = Var('z');
late SsaNode<SharedTypeView<Type>> xSsaNodeBeforeIf;
h.run([
declare(w, type: 'Object', initializer: expr('Object')),
declare(x, type: 'bool', initializer: expr('bool')),
declare(y, type: 'bool', initializer: expr('bool')),
declare(z, type: 'bool', initializer: expr('bool')),
x.write(w.is_('int')),
getSsaNodes((nodes) {
xSsaNodeBeforeIf = nodes[x]!;
expect(xSsaNodeBeforeIf.expressionInfo, isNotNull);
}),
if_(expr('bool'), [
y.write(w.is_('String')),
], [
z.write(w.is_('bool')),
]),
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 x = Var('x');
late SsaNode<SharedTypeView<Type>> xSsaNodeBeforeIf;
h.run([
declare(x, type: 'Object', initializer: expr('Object')),
getSsaNodes((nodes) {
xSsaNodeBeforeIf = nodes[x]!;
}),
if_(expr('bool'), [
x.write(expr('Object')),
return_(),
]),
getSsaNodes((nodes) {
expect(nodes[x], same(xSsaNodeBeforeIf));
}),
]);
});
test(
'ifStatement_end() ignores non-matching SSA info from "else" path if '
'unreachable', () {
var x = Var('x');
late SsaNode<SharedTypeView<Type>> xSsaNodeBeforeIf;
h.run([
declare(x, type: 'Object', initializer: expr('Object')),
getSsaNodes((nodes) {
xSsaNodeBeforeIf = nodes[x]!;
}),
if_(expr('bool'), [], [
x.write(expr('Object')),
return_(),
]),
getSsaNodes((nodes) {
expect(nodes[x], same(xSsaNodeBeforeIf));
}),
]);
});
test('initialize() promotes when not final', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int')),
checkPromoted(x, 'int'),
]);
});
test('initialize() does not promote when final', () {
var x = Var('x');
h.run([
declare(x, isFinal: true, type: 'int?', initializer: expr('int')),
checkNotPromoted(x),
]);
});
group('initialize() promotes implicitly typed vars to type parameter types',
() {
test('when not final', () {
h.addTypeVariable('T');
var x = Var('x');
h.run([
declare(x, initializer: expr('T&int')),
checkPromoted(x, 'T&int'),
]);
});
test('when final', () {
h.addTypeVariable('T');
var x = Var('x');
h.run([
declare(x,
isFinal: true,
initializer: expr('T&int'),
expectInferredType: 'T'),
checkPromoted(x, 'T&int'),
]);
});
});
group(
"initialize() doesn't promote explicitly typed vars to type "
'parameter types', () {
test('when not final', () {
var x = Var('x');
h.addTypeVariable('T');
h.run([
declare(x, type: 'T', initializer: expr('T&int')),
checkNotPromoted(x),
]);
});
test('when final', () {
var x = Var('x');
h.addTypeVariable('T');
h.run([
declare(x, isFinal: true, type: 'T', initializer: expr('T&int')),
checkNotPromoted(x),
]);
});
});
group(
"initialize() doesn't promote implicitly typed vars to ordinary types",
() {
test('when not final', () {
var x = Var('x');
h.run([
declare(x, initializer: expr('Null'), expectInferredType: 'dynamic'),
checkNotPromoted(x),
]);
});
test('when final', () {
var x = Var('x');
h.run([
declare(x,
isFinal: true,
initializer: expr('Null'),
expectInferredType: 'dynamic'),
checkNotPromoted(x),
]);
});
});
test('initialize() stores expressionInfo when not late', () {
var x = Var('x');
var y = Var('y');
h.run([
declare(y, type: 'int?', initializer: expr('int?')),
declare(x, type: 'Object', initializer: y.eq(nullLiteral)),
getSsaNodes((nodes) {
var info = nodes[x]!.expressionInfo!;
var key = h.promotionKeyStore.keyForVariable(y);
expect(info.after.promotionInfo!.get(h, key)!.promotedTypes, null);
expect(info.ifTrue.promotionInfo!.get(h, key)!.promotedTypes, null);
expect(
info.ifFalse.promotionInfo!
.get(h, key)!
.promotedTypes!
.single
.unwrapTypeView()
.type,
'int');
}),
]);
});
test('initialize() does not store expressionInfo when late', () {
var x = Var('x');
var y = Var('y');
h.run([
declare(y, type: 'int?', initializer: expr('int?')),
declare(x,
isLate: true, type: 'Object', initializer: y.eq(nullLiteral)),
getSsaNodes((nodes) {
expect(nodes[x]!.expressionInfo, isNull);
}),
]);
});
test(
'initialize() does not store expressionInfo for implicitly typed '
'vars, pre-bug fix', () {
h.disableRespectImplicitlyTypedVarInitializers();
var x = Var('x');
var y = Var('y');
h.run([
declare(y, type: 'int?', initializer: expr('int?')),
declare(x, initializer: y.eq(nullLiteral), expectInferredType: 'bool'),
getSsaNodes((nodes) {
expect(nodes[x]!.expressionInfo, isNull);
}),
]);
});
test(
'initialize() stores expressionInfo for implicitly typed '
'vars, post-bug fix', () {
var x = Var('x');
var y = Var('y');
h.run([
declare(y, type: 'int?', initializer: expr('int?')),
declare(x, initializer: y.eq(nullLiteral), expectInferredType: 'bool'),
getSsaNodes((nodes) {
expect(nodes[x]!.expressionInfo, isNotNull);
}),
]);
});
test(
'initialize() stores expressionInfo for explicitly typed '
'vars, pre-bug fix', () {
h.disableRespectImplicitlyTypedVarInitializers();
var x = Var('x');
var y = Var('y');
h.run([
declare(y, type: 'int?', initializer: expr('int?')),
declare(x, type: 'Object', initializer: y.eq(nullLiteral)),
getSsaNodes((nodes) {
expect(nodes[x]!.expressionInfo, isNotNull);
}),
]);
});
test('initialize() does not store expressionInfo for trivial expressions',
() {
var x = Var('x');
var y = Var('y');
h.run([
declare(y, type: 'int?', initializer: expr('int?')),
localFunction([
y.write(expr('int?')),
]),
declare(x,
type: 'Object',
// `y == null` is a trivial expression because y has been write
// captured.
initializer: y
.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 x = Var('x');
late SsaNode<SharedTypeView<Type>> ssaBeforePromotion;
h.run([
declare(x, type: declaredType, initializer: expr(declaredType)),
getSsaNodes((nodes) => ssaBeforePromotion = nodes[x]!),
if_(x.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', () {
h.run([
if_(expr('Null').is_('int'), [
checkReachable(true),
], [
checkReachable(true),
]),
]);
});
test('isExpression_end does nothing if applied to a non-variable, inverted',
() {
h.run([
if_(expr('Null').isNot('int'), [
checkReachable(true),
], [
checkReachable(true),
]),
]);
});
test('isExpression_end() does not promote write-captured vars', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
if_(x.is_('int'), [
checkPromoted(x, 'int'),
]),
localFunction([
x.write(expr('int?')),
]),
if_(x.is_('int'), [
checkNotPromoted(x),
]),
]);
});
test('isExpression_end() sets reachability for `this`', () {
h.thisType = 'C';
h.run([
if_(this_.is_('Never'), [
checkReachable(false),
], [
checkReachable(true),
]),
]);
});
group('isExpression_end() sets reachability for property gets', () {
test('on a variable', () {
h.addMember('C', 'f', 'Object?');
var x = Var('x');
h.run([
declare(x, type: 'C', initializer: expr('C')),
if_(x.property('f').is_('Never'), [
checkReachable(false),
], [
checkReachable(true),
]),
]);
});
test('on an arbitrary expression', () {
h.addMember('C', 'f', 'Object?');
h.run([
if_(expr('C').property('f').is_('Never'), [
checkReachable(false),
], [
checkReachable(true),
]),
]);
});
test('on explicit this', () {
h.thisType = 'C';
h.addMember('C', 'f', 'Object?');
h.run([
if_(this_.property('f').is_('Never'), [
checkReachable(false),
], [
checkReachable(true),
]),
]);
});
test('on implicit this/super', () {
h.thisType = 'C';
h.addMember('C', 'f', 'Object?');
h.run([
if_(thisProperty('f').is_('Never'), [
checkReachable(false),
], [
checkReachable(true),
]),
]);
});
});
test('isExpression_end() sets reachability for arbitrary exprs', () {
h.run([
if_(expr('int').is_('Never'), [
checkReachable(false),
], [
checkReachable(true),
]),
]);
});
test('labeledBlock without break', () {
var x = Var('x');
var l = Label('l');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
if_(x.isNot('int'), [
l.thenStmt(return_()),
]),
checkPromoted(x, 'int'),
]);
});
test('labeledBlock with break joins', () {
var x = Var('x');
var l = Label('l');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
if_(x.isNot('int'), [
l.thenStmt(block([
if_(expr('bool'), [
break_(l),
]),
return_(),
])),
]),
checkNotPromoted(x),
]);
});
test('logicalBinaryOp_rightBegin(isAnd: true) promotes in RHS', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
x.notEq(nullLiteral).and(second(checkPromoted(x, 'int'), expr('bool'))),
]);
});
test('logicalBinaryOp_rightEnd(isAnd: true) keeps promotions from RHS', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
if_(expr('bool').and(x.notEq(nullLiteral)), [
checkPromoted(x, 'int'),
]),
]);
});
test('logicalBinaryOp_rightEnd(isAnd: false) keeps promotions from RHS',
() {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
if_(expr('bool').or(x.eq(nullLiteral)), [], [
checkPromoted(x, 'int'),
]),
]);
});
test('logicalBinaryOp_rightBegin(isAnd: false) promotes in RHS', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
x.eq(nullLiteral).or(second(checkPromoted(x, 'int'), expr('bool'))),
]);
});
test('logicalBinaryOp(isAnd: true) joins promotions', () {
// if (x != null && y != null) {
// promotes x and y
// }
var x = Var('x');
var y = Var('y');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
declare(y, type: 'int?', initializer: expr('int?')),
if_(x.notEq(nullLiteral).and(y.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 x = Var('x');
var y = Var('y');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
declare(y, type: 'int?', initializer: expr('int?')),
if_(x.eq(nullLiteral).or(y.eq(nullLiteral)), [], [
checkPromoted(x, 'int'),
checkPromoted(y, 'int'),
]),
]);
});
test('logicalNot_end() inverts a condition', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
if_(x.eq(nullLiteral).not, [
checkPromoted(x, 'int'),
], [
checkNotPromoted(x),
]),
]);
});
test('logicalNot_end() handles null literals', () {
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 x = Var('x');
late SsaNode<SharedTypeView<Type>> ssaBeforePromotion;
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
getSsaNodes((nodes) => ssaBeforePromotion = nodes[x]!),
x.nonNullAssert,
checkPromoted(x, 'int'),
getSsaNodes((nodes) => expect(nodes[x], same(ssaBeforePromotion))),
]);
});
test('nonNullAssert_end sets reachability if type is `Null`', () {
// Note: this is handled by the general mechanism that marks control flow
// as reachable after any expression with static type `Never`. This is
// implemented in the flow analysis client, but we test it here anyway as
// a validation of the "mini AST" logic.
h.run([
expr('Null').nonNullAssert.thenStmt(checkReachable(false)),
]);
});
test('nullAwareAccess temporarily promotes', () {
var x = Var('x');
late SsaNode<SharedTypeView<Type>> ssaBeforePromotion;
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
getSsaNodes((nodes) => ssaBeforePromotion = nodes[x]!),
x.nullAwareAccess(second(
listLiteral(elementType: 'dynamic', [
checkReachable(true),
checkPromoted(x, 'int'),
getSsaNodes(
(nodes) => expect(nodes[x], same(ssaBeforePromotion))),
]),
expr('Null'))),
checkNotPromoted(x),
getSsaNodes((nodes) => expect(nodes[x], same(ssaBeforePromotion))),
]);
});
test('nullAwareAccess does not promote the target of a cascade', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
x.nullAwareAccess(
second(
listLiteral(elementType: 'dynamic', [
checkReachable(true),
checkNotPromoted(x),
]),
expr('Null')),
isCascaded: true),
]);
});
test('nullAwareAccess preserves demotions', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
x.as_('int'),
expr('int').nullAwareAccess(second(
listLiteral(elementType: 'dynamic', [
checkReachable(true),
checkPromoted(x, 'int'),
]),
x.write(expr('int?')))
.thenStmt(checkNotPromoted(x))),
checkNotPromoted(x),
]);
});
test('nullAwareAccess sets reachability correctly for `Null` type', () {
h.run([
expr('Null')
.nullAwareAccess(second(checkReachable(false), expr('Object?')))
.thenStmt(checkReachable(true)),
]);
});
test('nullAwareAccess_end ignores shorting if target is non-nullable', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
expr('int').nullAwareAccess(second(
listLiteral(elementType: 'dynamic', [
checkReachable(true),
x.as_('int'),
checkPromoted(x, 'int'),
]),
expr('Null'))),
// Since the null-shorting path was reachable, promotion of `x` should
// be cancelled.
checkNotPromoted(x),
]);
});
test('parenthesizedExpression preserves promotion behaviors', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
if_(x.parenthesized.notEq(nullLiteral.parenthesized).parenthesized, [
checkPromoted(x, 'int'),
]),
]);
});
test('ifCase splits control flow', () {
var x = Var('x');
var y = Var('y');
var z = Var('z');
var w = Var('w');
h.run([
declare(x, type: 'int'),
declare(y, type: 'int'),
declare(z, type: 'int'),
ifCase(
expr('num'),
w.pattern(type: 'int'),
[
x.write(expr('int')),
y.write(expr('int')),
],
[
y.write(expr('int')),
z.write(expr('int')),
],
),
checkAssigned(x, false),
checkAssigned(y, true),
checkAssigned(z, false),
]);
});
test('ifCase does not promote when expression true', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
ifCase(
x.notEq(nullLiteral),
intLiteral(0).pattern,
[
checkNotPromoted(x),
],
),
]);
});
test('promote promotes to a subtype and sets type of interest', () {
var x = Var('x');
h.run([
declare(x, type: 'num?', initializer: expr('num?')),
checkNotPromoted(x),
x.as_('num'),
checkPromoted(x, 'num'),
// Check that it's a type of interest by promoting and de-promoting.
if_(x.is_('int'), [
checkPromoted(x, 'int'),
x.write(expr('num')),
checkPromoted(x, 'num'),
]),
]);
});
test('promote does not promote to a non-subtype', () {
var x = Var('x');
h.run([
declare(x, type: 'num?', initializer: expr('num?')),
checkNotPromoted(x),
x.as_('String'),
checkNotPromoted(x),
]);
});
test('promote does not promote if variable is write-captured', () {
var x = Var('x');
h.run([
declare(x, type: 'num?', initializer: expr('num?')),
checkNotPromoted(x),
localFunction([
x.write(expr('num')),
]),
x.as_('num'),
checkNotPromoted(x),
]);
});
test('promotedType handles not-yet-seen variables', () {
// Note: this is needed for error recovery in the analyzer.
var x = Var('x');
h.run([
checkNotPromoted(x),
declare(x, type: 'int', initializer: expr('int')),
]);
});
test('switchExpression throw in scrutinee makes all cases unreachable', () {
h.run([
switchExpr(throw_(expr('C')), [
intLiteral(0)
.pattern
.thenExpr(second(checkReachable(false), intLiteral(1))),
default_.thenExpr(second(checkReachable(false), intLiteral(2))),
]),
checkReachable(false),
]);
});
test('switchExpression throw in case body has isolated effect', () {
h.run([
switchExpr(expr('int'), [
intLiteral(0).pattern.thenExpr(throw_(expr('C'))),
default_.thenExpr(second(checkReachable(true), intLiteral(2))),
]),
checkReachable(true),
]);
});
test('switchExpression throw in all case bodies affects flow after', () {
h.run([
switchExpr(expr('int'), [
intLiteral(0).pattern.thenExpr(throw_(expr('C'))),
default_.thenExpr(throw_(expr('C'))),
]),
checkReachable(false),
]);
});
test('switchExpression var promotes', () {
var x = Var('x');
h.run([
switchExpr(expr('int'), [
x
.pattern(type: 'int?')
.thenExpr(second(checkPromoted(x, 'int'), nullLiteral)),
]),
]);
});
test('switchStatement throw in scrutinee makes all cases unreachable', () {
h.run([
switch_(throw_(expr('int')), [
intLiteral(0).pattern.then([
checkReachable(false),
]),
intLiteral(1).pattern.then([
checkReachable(false),
]),
]),
checkReachable(false),
]);
});
test('switchStatement var promotes', () {
var x = Var('x');
h.run([
switch_(expr('int'), [
x.pattern(type: 'int?').then([
checkPromoted(x, 'int'),
]),
]),
]);
});
test('switchStatement_afterWhen() promotes', () {
var x = Var('x');
h.run([
switch_(expr('num'), [
x.pattern().when(x.is_('int')).then([
checkPromoted(x, 'int'),
]),
]),
]);
});
test('switchStatement_afterWhen() called for switch expressions', () {
var x = Var('x');
h.run([
switchExpr(expr('num'), [
x
.pattern()
.when(x.is_('int'))
.thenExpr(second(checkPromoted(x, 'int'), expr('String'))),
]),
]);
});
test('switchStatement_beginCase(false) restores previous promotions', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
x.as_('int'),
switch_(expr('int'), [
intLiteral(0).pattern.then([
checkPromoted(x, 'int'),
x.write(expr('int?')),
checkNotPromoted(x),
]),
intLiteral(1).pattern.then([
checkPromoted(x, 'int'),
x.write(expr('int?')),
checkNotPromoted(x),
]),
]),
]);
});
test('switchStatement_beginCase(false) does not un-promote', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
x.as_('int'),
switch_(expr('int'), [
intLiteral(0).pattern.then([
checkPromoted(x, 'int'),
x.write(expr('int?')),
checkNotPromoted(x),
])
]),
]);
});
test('switchStatement_beginCase(false) handles write captures in cases',
() {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
x.as_('int'),
switch_(
expr('int'),
[
intLiteral(0).pattern.then([
checkPromoted(x, 'int'),
localFunction([
x.write(expr('int?')),
]),
checkNotPromoted(x),
]),
],
),
]);
});
test('switchStatement_beginCase(true) un-promotes', () {
var x = Var('x');
late SsaNode<SharedTypeView<Type>> ssaBeforeSwitch;
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
x.as_('int'),
switch_(
expr('int').thenStmt(block([
checkPromoted(x, 'int'),
getSsaNodes((nodes) => ssaBeforeSwitch = nodes[x]!),
])),
[
switchStatementMember([
intLiteral(0).pattern,
], [
checkNotPromoted(x),
getSsaNodes((nodes) => expect(nodes[x], isNot(ssaBeforeSwitch))),
x.write(expr('int?')),
checkNotPromoted(x),
], hasLabels: true),
],
),
]);
});
test('switchStatement_beginCase(true) handles write captures in cases', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
x.as_('int'),
switch_(
expr('int'),
[
switchStatementMember([
intLiteral(0).pattern,
], [
x.as_('int'),
checkNotPromoted(x),
localFunction([
x.write(expr('int?')),
]),
checkNotPromoted(x),
], hasLabels: true),
],
),
]);
});
test('switchStatement_end(false) joins break and default', () {
var x = Var('x');
var y = Var('y');
var z = Var('z');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
declare(y, type: 'int?', initializer: expr('int?')),
declare(z, type: 'int?', initializer: expr('int?')),
y.as_('int'),
z.as_('int'),
switch_(expr('int'), [
intLiteral(0).pattern.then([
x.as_('int'),
y.write(expr('int?')),
break_(),
]),
]),
checkNotPromoted(x),
checkNotPromoted(y),
checkPromoted(z, 'int'),
]);
});
test('switchStatement_end(true) joins breaks', () {
var w = Var('w');
var x = Var('x');
var y = Var('y');
var z = Var('z');
h.run([
declare(w, type: 'int?', initializer: expr('int?')),
declare(x, type: 'int?', initializer: expr('int?')),
declare(y, type: 'int?', initializer: expr('int?')),
declare(z, type: 'int?', initializer: expr('int?')),
x.as_('int'),
y.as_('int'),
z.as_('int'),
switch_(expr('int'), [
intLiteral(0).pattern.then([
w.as_('int'),
y.as_('int'),
x.write(expr('int?')),
break_(),
]),
default_.then([
w.as_('int'),
x.as_('int'),
y.write(expr('int?')),
break_(),
]),
]),
checkPromoted(w, 'int'),
checkNotPromoted(x),
checkNotPromoted(y),
checkPromoted(z, 'int'),
]);
});
test('switchStatement_end(true) allows fall-through of last case', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
switch_(expr('int'), [
intLiteral(0).pattern.then([
x.as_('int'),
break_(),
]),
default_.then([]),
]),
checkNotPromoted(x),
]);
});
test('switchStatement_endAlternative() joins branches', () {
var x1 = Var('x', identity: 'x1');
var x2 = Var('x', identity: 'x2');
PatternVariableJoin('x', expectedComponents: [x1, x2]);
var y = Var('y');
var z = Var('z');
h.run([
declare(y, type: 'num'),
declare(z, type: 'num'),
switch_(
expr('num'),
[
switchStatementMember([
x1.pattern().when(x1.is_('int').and(y.is_('int'))),
x2.pattern().when(y.is_('int').and(z.is_('int'))),
], [
checkNotPromoted(x2),
checkPromoted(y, 'int'),
checkNotPromoted(z),
]),
],
),
]);
});
test('tryCatchStatement_bodyEnd() restores pre-try state', () {
var x = Var('x');
var y = Var('y');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
declare(y, type: 'int?', initializer: expr('int?')),
y.as_('int'),
try_([
x.as_('int'),
checkPromoted(x, 'int'),
checkPromoted(y, 'int'),
]).catch_(body: [
checkNotPromoted(x),
checkPromoted(y, 'int'),
]),
]);
});
test('tryCatchStatement_bodyEnd() un-promotes variables assigned in body',
() {
var x = Var('x');
late SsaNode<SharedTypeView<Type>> ssaAfterTry;
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
x.as_('int'),
checkPromoted(x, 'int'),
try_([
x.write(expr('int?')),
x.as_('int'),
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 x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
x.as_('int'),
checkPromoted(x, 'int'),
try_([
localFunction([
x.write(expr('int?')),
]),
return_(),
]).catch_(body: [
x.as_('int'),
checkNotPromoted(x),
]),
]);
});
test('tryCatchStatement_catchBegin() restores previous post-body state',
() {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
try_([]).catch_(body: [
x.as_('int'),
checkPromoted(x, 'int'),
]).catch_(body: [
checkNotPromoted(x),
]),
]);
});
test('tryCatchStatement_catchBegin() initializes vars', () {
var e = Var('e');
var st = Var('st');
h.run([
try_([]).catch_(exception: e, stackTrace: st, body: [
checkAssigned(e, true),
checkAssigned(st, true),
]),
]);
});
test('tryCatchStatement_catchEnd() joins catch state with after-try state',
() {
var x = Var('x');
var y = Var('y');
var z = Var('z');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
declare(y, type: 'int?', initializer: expr('int?')),
declare(z, type: 'int?', initializer: expr('int?')),
try_([
x.as_('int'),
y.as_('int'),
]).catch_(body: [
x.as_('int'),
z.as_('int'),
]),
// 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 x = Var('x');
var y = Var('y');
var z = Var('z');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
declare(y, type: 'int?', initializer: expr('int?')),
declare(z, type: 'int?', initializer: expr('int?')),
try_([
return_(),
]).catch_(body: [
x.as_('int'),
y.as_('int'),
]).catch_(body: [
x.as_('int'),
z.as_('int'),
]),
// 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 x = Var('x');
var y = Var('y');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
declare(y, type: 'int?', initializer: expr('int?')),
y.as_('int'),
try_([
x.as_('int'),
checkPromoted(x, 'int'),
checkPromoted(y, 'int'),
]).finally_([
checkNotPromoted(x),
checkPromoted(y, 'int'),
]),
]);
});
test(
'tryFinallyStatement_finallyBegin() un-promotes variables assigned in '
'body', () {
var x = Var('x');
late SsaNode<SharedTypeView<Type>> ssaAtStartOfTry;
late SsaNode<SharedTypeView<Type>> ssaAfterTry;
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
x.as_('int'),
checkPromoted(x, 'int'),
try_([
getSsaNodes((nodes) => ssaAtStartOfTry = nodes[x]!),
x.write(expr('int?')),
x.as_('int'),
checkPromoted(x, 'int'),
getSsaNodes((nodes) => ssaAfterTry = nodes[x]!),
]).finally_([
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 x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
try_([
localFunction([
x.write(expr('int?')),
]),
return_(),
]).finally_([
x.as_('int'),
checkNotPromoted(x),
]),
]);
});
test('tryFinallyStatement_end() restores promotions from try body', () {
var x = Var('x');
var y = Var('y');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
declare(y, type: 'int?', initializer: expr('int?')),
try_([
x.as_('int'),
checkPromoted(x, 'int'),
]).finally_([
checkNotPromoted(x),
y.as_('int'),
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 x = Var('x');
var y = Var('y');
late SsaNode<SharedTypeView<Type>> xSsaAtEndOfFinally;
late SsaNode<SharedTypeView<Type>> ySsaAtEndOfFinally;
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
declare(y, type: 'int?', initializer: expr('int?')),
try_([
x.as_('int'),
checkPromoted(x, 'int'),
]).finally_([
checkNotPromoted(x),
x.write(expr('int?')),
y.write(expr('int?')),
y.as_('int'),
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 x = Var('x');
var y = Var('y');
late SsaNode<SharedTypeView<Type>> xSsaAtEndOfTry;
late SsaNode<SharedTypeView<Type>> ySsaAtEndOfTry;
late SsaNode<SharedTypeView<Type>> xSsaAtEndOfFinally;
late SsaNode<SharedTypeView<Type>> ySsaAtEndOfFinally;
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
declare(y, type: 'int?', initializer: expr('int?')),
try_([
x.write(expr('int?')),
y.write(expr('int?')),
getSsaNodes((nodes) {
xSsaAtEndOfTry = nodes[x]!;
ySsaAtEndOfTry = nodes[y]!;
}),
]).finally_([
if_(expr('bool'), [
x.write(expr('int?')),
]),
if_(expr('bool'), [
y.write(expr('int?')),
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', () {
h.run([
try_([
return_(),
checkReachable(false),
]).finally_([
checkReachable(true),
]),
checkReachable(false),
]);
});
test(
'tryFinallyStatement_end() sets unreachable if end of finally block '
'unreachable', () {
h.run([
try_([
checkReachable(true),
]).finally_([
return_(),
checkReachable(false),
]),
checkReachable(false),
]);
});
test(
'tryFinallyStatement_end() handles a variable declared only in the '
'try block', () {
var x = Var('x');
h.run([
try_([
declare(x, type: 'int?', initializer: expr('int?')),
]).finally_([]),
]);
});
test(
'tryFinallyStatement_end() handles a variable declared only in the '
'finally block', () {
var x = Var('x');
h.run([
try_([]).finally_([
declare(x, type: 'int?', initializer: expr('int?')),
]),
]);
});
test(
'tryFinallyStatement_end() handles a variable that was write '
'captured in the try block', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
try_([
localFunction([
x.write(expr('int?')),
]),
]).finally_([]),
if_(x.notEq(nullLiteral), [
checkNotPromoted(x),
]),
]);
});
test(
'tryFinallyStatement_end() handles a variable that was write '
'captured in the finally block', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
try_([]).finally_([
localFunction([
x.write(expr('int?')),
]),
]),
if_(x.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 x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
try_([
if_(x.eq(nullLiteral), [
return_(),
]),
checkPromoted(x, 'int'),
]).finally_([
localFunction([
x.write(expr('int?')),
]),
]),
// The capture in the `finally` cancels old promotions and prevents
// future promotions.
checkNotPromoted(x),
if_(x.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 x = Var('x');
h.run([
declare(x, type: 'Object', initializer: expr('Object')),
try_([
if_(x.is_('num', isInverted: true), [
return_(),
]),
checkPromoted(x, 'num'),
]).finally_([
if_(x.is_('int', isInverted: true), [
return_(),
]),
]),
// The promotion chain now contains both `num` and `int`.
checkPromoted(x, 'int'),
x.write(expr('num')),
checkPromoted(x, 'num'),
]);
});
test(
'tryFinallyStatement_end() keeps promotions from the finally block '
'when there is a write in the finally block', () {
var x = Var('x');
h.run([
declare(x, type: 'Object', initializer: expr('Object')),
try_([
if_(x.is_('String', isInverted: true), [
return_(),
]),
checkPromoted(x, 'String'),
]).finally_([
x.write(expr('Object')),
if_(x.is_('int', isInverted: true), [
return_(),
]),
]),
checkPromoted(x, 'int'),
]);
});
test(
'tryFinallyStatement_end() keeps tests from both the try and finally '
'blocks', () {
var x = Var('x');
h.run([
declare(x, type: 'Object', initializer: expr('Object')),
try_([
if_(x.is_('String', isInverted: true), []),
checkNotPromoted(x),
]).finally_([
if_(x.is_('int', isInverted: true), []),
checkNotPromoted(x),
]),
checkNotPromoted(x),
if_(expr('bool'), [
x.write(expr('String')),
checkPromoted(x, 'String'),
], [
x.write(expr('int')),
checkPromoted(x, 'int'),
]),
]);
});
test(
'tryFinallyStatement_end() handles variables not definitely assigned '
'in either the try or finally block', () {
var x = Var('x');
h.run([
declare(x, type: 'Object'),
checkAssigned(x, false),
try_([
if_(expr('bool'), [
x.write(expr('Object')),
]),
checkAssigned(x, false),
]).finally_([
if_(expr('bool'), [
x.write(expr('Object')),
]),
checkAssigned(x, false),
]),
checkAssigned(x, false),
]);
});
test(
'tryFinallyStatement_end() handles variables definitely assigned in '
'the try block', () {
var x = Var('x');
h.run([
declare(x, type: 'Object'),
checkAssigned(x, false),
try_([
x.write(expr('Object')),
checkAssigned(x, true),
]).finally_([
if_(expr('bool'), [
x.write(expr('Object')),
]),
checkAssigned(x, false),
]),
checkAssigned(x, true),
]);
});
test(
'tryFinallyStatement_end() handles variables definitely assigned in '
'the finally block', () {
var x = Var('x');
h.run([
declare(x, type: 'Object'),
checkAssigned(x, false),
try_([
if_(expr('bool'), [
x.write(expr('Object')),
]),
checkAssigned(x, false),
]).finally_([
x.write(expr('Object')),
checkAssigned(x, true),
]),
checkAssigned(x, true),
]);
});
test(
'tryFinallyStatement_end() handles variables definitely unassigned '
'in both the try and finally blocks', () {
var x = Var('x');
h.run([
declare(x, type: 'Object'),
checkUnassigned(x, true),
try_([
checkUnassigned(x, true),
]).finally_([
checkUnassigned(x, true),
]),
checkUnassigned(x, true),
]);
});
test(
'tryFinallyStatement_end() handles variables definitely unassigned '
'in the try but not the finally block', () {
var x = Var('x');
h.run([
declare(x, type: 'Object'),
checkUnassigned(x, true),
try_([
checkUnassigned(x, true),
]).finally_([
if_(expr('bool'), [
x.write(expr('Object')),
]),
checkUnassigned(x, false),
]),
checkUnassigned(x, false),
]);
});
test(
'tryFinallyStatement_end() handles variables definitely unassigned '
'in the finally but not the try block', () {
var x = Var('x');
h.run([
declare(x, type: 'Object'),
checkUnassigned(x, true),
try_([
if_(expr('bool'), [
x.write(expr('Object')),
]),
checkUnassigned(x, false),
]).finally_([
checkUnassigned(x, false),
]),
checkUnassigned(x, false),
]);
});
});
test('variableRead() restores promotions from previous write()', () {
var x = Var('x');
var y = Var('y');
var z = Var('z');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
declare(y, type: 'int?', initializer: expr('int?')),
declare(z, type: 'bool', initializer: expr('bool')),
// Create a variable that promotes x if its value is true, and y if its
// value is false.
z.write(x.notEq(nullLiteral).conditional(
booleanLiteral(true),
y
.notEq(nullLiteral)
.conditional(booleanLiteral(false), throw_(expr('Object'))))),
checkNotPromoted(x),
checkNotPromoted(y),
// Simply reading the variable shouldn't promote anything.
z,
checkNotPromoted(x),
checkNotPromoted(y),
// But reading it in an "if" condition should promote.
if_(z, [
checkPromoted(x, 'int'),
checkNotPromoted(y),
], [
checkNotPromoted(x),
checkPromoted(y, 'int'),
]),