| // 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) |
|