| // 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'), |
| ]), |
| |