| // 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'), |
| ]), |
| ]); |
| }); |
| |
| test('variableRead() restores promotions from previous initialization', () { |
| 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?')), |
| // Create a variable that promotes x if its value is true, and y if its |
| // value is false. |
| declare(z, |
| initializer: 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'), |
| ]), |
| ]); |
| }); |
| |
| test('variableRead() rebases old promotions', () { |
| 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: '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(w), |
| checkNotPromoted(x), |
| checkNotPromoted(y), |
| w.nonNullAssert, |
| 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, [ |
| 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 x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(x, type: 'int?', initializer: expr('int?')), |
| declare(y, type: 'int?', initializer: expr('int?')), |
| y.write(nullLiteral), |
| checkNotPromoted(x), |
| checkNotPromoted(y), |
| if_(x.eq(y), [ |
| 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 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]!), |
| while_( |
| second( |
| listLiteral(elementType: 'dynamic', [ |
| checkNotPromoted(x), |
| getSsaNodes( |
| (nodes) => expect(nodes[x], isNot(ssaBeforeLoop))), |
| ]), |
| expr('bool')), |
| [ |
| x.write(expr('Null')), |
| ]), |
| ]); |
| }); |
| |
| test('whileStatement_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'), |
| while_( |
| second( |
| listLiteral(elementType: 'dynamic', [ |
| x.as_('int'), |
| checkNotPromoted(x), |
| localFunction([ |
| x.write(expr('int?')), |
| ]), |
| ]), |
| expr('bool')), |
| []), |
| ]); |
| }); |
| |
| test('whileStatement_bodyBegin() promotes', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'int?', initializer: expr('int?')), |
| while_(x.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 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?')), |
| while_(x.eq(nullLiteral).or(z.eq(nullLiteral)), [ |
| if_(expr('bool'), [ |
| x.as_('int'), |
| y.as_('int'), |
| break_(), |
| ]), |
| ]), |
| checkPromoted(x, 'int'), |
| checkNotPromoted(y), |
| checkNotPromoted(z), |
| ]); |
| }); |
| |
| test('whileStatement_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?')), |
| while_(expr('bool'), [ |
| 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 |
| // 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 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?')), |
| while_(expr('bool'), [ |
| 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('write() de-promotes and updates Ssa of a promoted variable', () { |
| var x = Var('x'); |
| var y = Var('y'); |
| late SsaNode<SharedTypeView<Type>> ssaBeforeWrite; |
| late ExpressionInfo<SharedTypeView<Type>> writtenValueInfo; |
| h.run([ |
| declare(x, type: 'Object', initializer: expr('Object')), |
| declare(y, type: 'int?', initializer: expr('int?')), |
| x.as_('int'), |
| checkPromoted(x, 'int'), |
| getSsaNodes((nodes) => ssaBeforeWrite = nodes[x]!), |
| x.write(y.eq(nullLiteral).getExpressionInfo((info) { |
| expect(info, isNotNull); |
| writtenValueInfo = info!; |
| })), |
| checkNotPromoted(x), |
| getSsaNodes((nodes) { |
| expect(nodes[x], isNot(ssaBeforeWrite)); |
| expect(nodes[x]!.expressionInfo, same(writtenValueInfo)); |
| }), |
| ]); |
| }); |
| |
| test('write() updates Ssa', () { |
| var x = Var('x'); |
| var y = Var('y'); |
| late SsaNode<SharedTypeView<Type>> ssaBeforeWrite; |
| late ExpressionInfo<SharedTypeView<Type>> writtenValueInfo; |
| h.run([ |
| declare(x, type: 'Object', initializer: expr('Object')), |
| declare(y, type: 'int?', initializer: expr('int?')), |
| getSsaNodes((nodes) => ssaBeforeWrite = nodes[x]!), |
| x.write(y.eq(nullLiteral).getExpressionInfo((info) { |
| expect(info, isNotNull); |
| writtenValueInfo = info!; |
| })), |
| 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 x = Var('x'); |
| var y = Var('y'); |
| late SsaNode<SharedTypeView<Type>> xSsaBeforeWrite; |
| late SsaNode<SharedTypeView<Type>> ySsa; |
| h.run([ |
| declare(x, type: 'int?', initializer: expr('int?')), |
| declare(y, type: 'int?', initializer: expr('int?')), |
| getSsaNodes((nodes) { |
| xSsaBeforeWrite = nodes[x]!; |
| ySsa = nodes[y]!; |
| }), |
| x.write(y), |
| getSsaNodes((nodes) { |
| expect(nodes[x], isNot(xSsaBeforeWrite)); |
| expect(nodes[x], isNot(ySsa)); |
| }), |
| ]); |
| }); |
| |
| test('write() does not store expressionInfo for trivial expressions', () { |
| var x = Var('x'); |
| var y = Var('y'); |
| late SsaNode<SharedTypeView<Type>> ssaBeforeWrite; |
| h.run([ |
| declare(x, type: 'Object', initializer: expr('Object')), |
| declare(y, type: 'int?', initializer: expr('int?')), |
| localFunction([ |
| y.write(expr('int?')), |
| ]), |
| getSsaNodes((nodes) => ssaBeforeWrite = nodes[x]!), |
| // `y == null` is a trivial expression because y has been write |
| // captured. |
| x.write(y |
| .eq(nullLiteral) |
| .getExpressionInfo((info) => expect(info, isNotNull))), |
| getSsaNodes((nodes) { |
| expect(nodes[x], isNot(ssaBeforeWrite)); |
| expect(nodes[x]!.expressionInfo, isNull); |
| }), |
| ]); |
| }); |
| |
| test('write() permits expression to be null', () { |
| var x = Var('x'); |
| late SsaNode<SharedTypeView<Type>> ssaBeforeWrite; |
| h.run([ |
| declare(x, type: 'Object', initializer: expr('Object')), |
| getSsaNodes((nodes) => ssaBeforeWrite = nodes[x]!), |
| x.write(null), |
| getSsaNodes((nodes) { |
| expect(nodes[x], isNot(ssaBeforeWrite)); |
| expect(nodes[x]!.expressionInfo, isNull); |
| }), |
| ]); |
| }); |
| |
| test('Infinite loop does not implicitly assign variables', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'int'), |
| while_(booleanLiteral(true), [ |
| x.write(expr('Null')), |
| ]), |
| checkAssigned(x, false), |
| ]); |
| }); |
| |
| test('If(false) does not discard promotions', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object', initializer: expr('Object')), |
| x.as_('int'), |
| checkPromoted(x, 'int'), |
| if_(booleanLiteral(false), [ |
| checkPromoted(x, 'int'), |
| ]), |
| ]); |
| }); |
| |
| test('Promotions do not occur when a variable is write-captured', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object', initializer: expr('Object')), |
| localFunction([ |
| x.write(expr('Object')), |
| ]), |
| getSsaNodes((nodes) => expect(nodes[x], isNull)), |
| x.as_('int'), |
| checkNotPromoted(x), |
| getSsaNodes((nodes) => expect(nodes[x], isNull)), |
| ]); |
| }); |
| |
| test('Promotion cancellation of write-captured vars survives join', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object', initializer: expr('Object')), |
| if_(expr('bool'), [ |
| localFunction([ |
| x.write(expr('Object')), |
| ]), |
| ], [ |
| // Promotion should work here because the write capture is in the |
| // other branch. |
| x.as_('int'), checkPromoted(x, 'int'), |
| ]), |
| // But the promotion should be cancelled now, after the join. |
| checkNotPromoted(x), |
| // And further attempts to promote should fail due to the write capture. |
| x.as_('int'), checkNotPromoted(x), |
| ]); |
| }); |
| |
| test('issue 47991', () { |
| var b = Var('b'); |
| var i = Var('i'); |
| h.run([ |
| localFunction([ |
| declare(b, type: 'bool', initializer: expr('bool').or(expr('bool'))), |
| declare(i, isFinal: true, type: 'int'), |
| if_(b, [ |
| checkUnassigned(i, true), |
| i.write(expr('int')), |
| ], [ |
| checkUnassigned(i, true), |
| i.write(expr('int')), |
| ]), |
| ]), |
| ]); |
| }); |
| }); |
| |
| group('Reachability', () { |
| test('initial state', () { |
| expect(Reachability.initial.parent, isNull); |
| expect(Reachability.initial.locallyReachable, true); |
| expect(Reachability.initial.overallReachable, true); |
| }); |
| |
| test('split', () { |
| var reachableSplit = Reachability.initial.split(); |
| expect(reachableSplit.parent, same(Reachability.initial)); |
| expect(reachableSplit.overallReachable, true); |
| expect(reachableSplit.locallyReachable, true); |
| var unreachable = reachableSplit.setUnreachable(); |
| var unreachableSplit = unreachable.split(); |
| expect(unreachableSplit.parent, same(unreachable)); |
| expect(unreachableSplit.overallReachable, false); |
| expect(unreachableSplit.locallyReachable, true); |
| }); |
| |
| test('unsplit', () { |
| var base = Reachability.initial.split(); |
| var reachableSplit = base.split(); |
| var reachableSplitUnsplit = reachableSplit.unsplit(); |
| expect(reachableSplitUnsplit.parent, same(base.parent)); |
| expect(reachableSplitUnsplit.overallReachable, true); |
| expect(reachableSplitUnsplit.locallyReachable, true); |
| var reachableSplitUnreachable = reachableSplit.setUnreachable(); |
| var reachableSplitUnreachableUnsplit = |
| reachableSplitUnreachable.unsplit(); |
| expect(reachableSplitUnreachableUnsplit.parent, same(base.parent)); |
| expect(reachableSplitUnreachableUnsplit.overallReachable, false); |
| expect(reachableSplitUnreachableUnsplit.locallyReachable, false); |
| var unreachable = base.setUnreachable(); |
| var unreachableSplit = unreachable.split(); |
| var unreachableSplitUnsplit = unreachableSplit.unsplit(); |
| expect(unreachableSplitUnsplit, same(unreachable)); |
| var unreachableSplitUnreachable = unreachableSplit.setUnreachable(); |
| var unreachableSplitUnreachableUnsplit = |
| unreachableSplitUnreachable.unsplit(); |
| expect(unreachableSplitUnreachableUnsplit, same(unreachable)); |
| }); |
| |
| test('setUnreachable', () { |
| var reachable = Reachability.initial.split(); |
| var unreachable = reachable.setUnreachable(); |
| expect(unreachable.parent, same(reachable.parent)); |
| expect(unreachable.locallyReachable, false); |
| expect(unreachable.overallReachable, false); |
| expect(unreachable.setUnreachable(), same(unreachable)); |
| var provisionallyReachable = unreachable.split(); |
| var provisionallyUnreachable = provisionallyReachable.setUnreachable(); |
| expect( |
| provisionallyUnreachable.parent, same(provisionallyReachable.parent)); |
| expect(provisionallyUnreachable.locallyReachable, false); |
| expect(provisionallyUnreachable.overallReachable, false); |
| expect(provisionallyUnreachable.setUnreachable(), |
| same(provisionallyUnreachable)); |
| }); |
| |
| test('rebaseForward', () { |
| var previous = Reachability.initial; |
| var reachable = previous.split(); |
| var reachable2 = previous.split(); |
| var unreachable = reachable.setUnreachable(); |
| var unreachablePrevious = previous.setUnreachable(); |
| var reachable3 = unreachablePrevious.split(); |
| expect(reachable.rebaseForward(reachable), same(reachable)); |
| expect(reachable.rebaseForward(reachable2), same(reachable2)); |
| expect(reachable.rebaseForward(unreachable), same(unreachable)); |
| expect(unreachable.rebaseForward(reachable).parent, same(previous)); |
| expect(unreachable.rebaseForward(reachable).locallyReachable, false); |
| expect(unreachable.rebaseForward(unreachable), same(unreachable)); |
| expect(reachable.rebaseForward(unreachablePrevious), |
| same(unreachablePrevious)); |
| expect( |
| unreachablePrevious.rebaseForward(reachable).parent, same(previous)); |
| expect( |
| unreachablePrevious.rebaseForward(reachable).locallyReachable, false); |
| expect(reachable.rebaseForward(reachable3), same(reachable3)); |
| expect(reachable3.rebaseForward(reachable).parent, same(previous)); |
| expect(reachable3.rebaseForward(reachable).locallyReachable, false); |
| }); |
| |
| test('commonAncestor', () { |
| var parent1 = Reachability.initial; |
| var parent2 = parent1.setUnreachable(); |
| var child1 = parent1.split(); |
| var child2 = parent1.split(); |
| var child3 = child1.split(); |
| var child4 = child2.split(); |
| expect(Reachability.commonAncestor(null, null), null); |
| expect(Reachability.commonAncestor(null, parent1), null); |
| expect(Reachability.commonAncestor(parent1, null), null); |
| expect(Reachability.commonAncestor(null, child1), null); |
| expect(Reachability.commonAncestor(child1, null), null); |
| expect(Reachability.commonAncestor(null, child3), null); |
| expect(Reachability.commonAncestor(child3, null), null); |
| expect(Reachability.commonAncestor(parent1, parent1), same(parent1)); |
| expect(Reachability.commonAncestor(parent1, parent2), null); |
| expect(Reachability.commonAncestor(parent2, child1), null); |
| expect(Reachability.commonAncestor(child1, parent2), null); |
| expect(Reachability.commonAncestor(parent2, child3), null); |
| expect(Reachability.commonAncestor(child3, parent2), null); |
| expect(Reachability.commonAncestor(parent1, child1), same(parent1)); |
| expect(Reachability.commonAncestor(child1, parent1), same(parent1)); |
| expect(Reachability.commonAncestor(parent1, child3), same(parent1)); |
| expect(Reachability.commonAncestor(child3, parent1), same(parent1)); |
| expect(Reachability.commonAncestor(child1, child1), same(child1)); |
| expect(Reachability.commonAncestor(child1, child2), same(parent1)); |
| expect(Reachability.commonAncestor(child1, child3), same(child1)); |
| expect(Reachability.commonAncestor(child3, child1), same(child1)); |
| expect(Reachability.commonAncestor(child1, child4), same(parent1)); |
| expect(Reachability.commonAncestor(child4, child1), same(parent1)); |
| expect(Reachability.commonAncestor(child3, child3), same(child3)); |
| expect(Reachability.commonAncestor(child3, child4), same(parent1)); |
| }); |
| }); |
| |
| group('State', () { |
| var intVar = Var('x')..type = Type('int'); |
| var intQVar = Var('x')..type = Type('int?'); |
| var objectQVar = Var('x')..type = Type('Object?'); |
| var nullVar = Var('x')..type = Type('Null'); |
| |
| group('setUnreachable', () { |
| var unreachable = FlowModel<SharedTypeView<Type>>( |
| Reachability.initial.setUnreachable()); |
| var reachable = FlowModel<SharedTypeView<Type>>(Reachability.initial); |
| test('unchanged', () { |
| expect(unreachable.setUnreachable(), same(unreachable)); |
| }); |
| |
| test('changed', () { |
| void _check(FlowModel<SharedTypeView<Type>> initial) { |
| var s = initial.setUnreachable(); |
| expect(s, isNot(same(initial))); |
| expect(s.reachable.overallReachable, false); |
| expect(s.promotionInfo, same(initial.promotionInfo)); |
| } |
| |
| _check(reachable); |
| }); |
| }); |
| |
| test('split', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial); |
| var s2 = s1.split(); |
| expect(s2.reachable.parent, same(s1.reachable)); |
| }); |
| |
| test('unsplit', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial.split()); |
| var s2 = s1.unsplit(); |
| expect(s2.reachable, same(Reachability.initial)); |
| }); |
| |
| group('unsplitTo', () { |
| test('no change', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial.split()); |
| var result = s1.unsplitTo(s1.reachable.parent!); |
| expect(result, same(s1)); |
| }); |
| |
| test('unsplit once, reachable', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial.split()); |
| var s2 = s1.split(); |
| var result = s2.unsplitTo(s1.reachable.parent!); |
| expect(result.reachable, same(s1.reachable)); |
| }); |
| |
| test('unsplit once, unreachable', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial.split()); |
| var s2 = s1.split().setUnreachable(); |
| var result = s2.unsplitTo(s1.reachable.parent!); |
| expect(result.reachable.locallyReachable, false); |
| expect(result.reachable.parent, same(s1.reachable.parent)); |
| }); |
| |
| test('unsplit twice, reachable', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial.split()); |
| var s2 = s1.split(); |
| var s3 = s2.split(); |
| var result = s3.unsplitTo(s1.reachable.parent!); |
| expect(result.reachable, same(s1.reachable)); |
| }); |
| |
| test('unsplit twice, top unreachable', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial.split()); |
| var s2 = s1.split(); |
| var s3 = s2.split().setUnreachable(); |
| var result = s3.unsplitTo(s1.reachable.parent!); |
| expect(result.reachable.locallyReachable, false); |
| expect(result.reachable.parent, same(s1.reachable.parent)); |
| }); |
| |
| test('unsplit twice, previous unreachable', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial.split()); |
| var s2 = s1.split().setUnreachable(); |
| var s3 = s2.split(); |
| var result = s3.unsplitTo(s1.reachable.parent!); |
| expect(result.reachable.locallyReachable, false); |
| expect(result.reachable.parent, same(s1.reachable.parent)); |
| }); |
| }); |
| |
| group('tryPromoteForTypeCheck', () { |
| test('unpromoted -> unchanged (same)', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial); |
| var s2 = s1._tryPromoteForTypeCheck(h, intVar, 'int').ifTrue; |
| expect(s2, same(s1)); |
| }); |
| |
| test('unpromoted -> unchanged (supertype)', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial); |
| var s2 = s1._tryPromoteForTypeCheck(h, intVar, 'Object').ifTrue; |
| expect(s2, same(s1)); |
| }); |
| |
| test('unpromoted -> unchanged (unrelated)', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial); |
| var s2 = s1._tryPromoteForTypeCheck(h, intVar, 'String').ifTrue; |
| expect(s2, same(s1)); |
| }); |
| |
| test('unpromoted -> subtype', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial); |
| var s2 = s1._tryPromoteForTypeCheck(h, intQVar, 'int').ifTrue; |
| expect(s2.reachable.overallReachable, true); |
| expect(s2.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(intQVar): |
| _matchVariableModel(chain: ['int'], ofInterest: ['int']) |
| }); |
| }); |
| |
| test('promoted -> unchanged (same)', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._tryPromoteForTypeCheck(h, objectQVar, 'int') |
| .ifTrue; |
| var s2 = s1._tryPromoteForTypeCheck(h, objectQVar, 'int').ifTrue; |
| expect(s2, same(s1)); |
| }); |
| |
| test('promoted -> unchanged (supertype)', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._tryPromoteForTypeCheck(h, objectQVar, 'int') |
| .ifTrue; |
| var s2 = s1._tryPromoteForTypeCheck(h, objectQVar, 'Object').ifTrue; |
| expect(s2, same(s1)); |
| }); |
| |
| test('promoted -> unchanged (unrelated)', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._tryPromoteForTypeCheck(h, objectQVar, 'int') |
| .ifTrue; |
| var s2 = s1._tryPromoteForTypeCheck(h, objectQVar, 'String').ifTrue; |
| expect(s2, same(s1)); |
| }); |
| |
| test('promoted -> subtype', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._tryPromoteForTypeCheck(h, objectQVar, 'int?') |
| .ifTrue; |
| var s2 = s1._tryPromoteForTypeCheck(h, objectQVar, 'int').ifTrue; |
| expect(s2.reachable.overallReachable, true); |
| expect(s2.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(objectQVar): _matchVariableModel( |
| chain: ['int?', 'int'], ofInterest: ['int?', 'int']) |
| }); |
| }); |
| }); |
| |
| group('write', () { |
| var objectQVar = Var('x')..type = Type('Object?'); |
| |
| test('without declaration', () { |
| // This should not happen in valid code, but test that we don't crash. |
| |
| var s = FlowModel<SharedTypeView<Type>>(Reachability.initial)._write( |
| h, |
| null, |
| objectQVar, |
| SharedTypeView(Type('Object?')), |
| new SsaNode<SharedTypeView<Type>>(null)); |
| expect( |
| s.promotionInfo |
| ?.get(h, h.promotionKeyStore.keyForVariable(objectQVar)), |
| isNull); |
| }); |
| |
| test('unchanged', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._declare(h, objectQVar, true); |
| var s2 = s1._write(h, null, objectQVar, SharedTypeView(Type('Object?')), |
| new SsaNode<SharedTypeView<Type>>(null)); |
| expect(s2, isNot(same(s1))); |
| expect(s2.reachable, same(s1.reachable)); |
| expect( |
| s2._infoFor(h, objectQVar), |
| _matchVariableModel( |
| chain: null, |
| ofInterest: isEmpty, |
| assigned: true, |
| unassigned: false)); |
| }); |
| |
| test('marks as assigned', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._declare(h, objectQVar, false); |
| var s2 = s1._write(h, null, objectQVar, SharedTypeView(Type('int?')), |
| new SsaNode<SharedTypeView<Type>>(null)); |
| expect(s2.reachable.overallReachable, true); |
| expect( |
| s2._infoFor(h, objectQVar), |
| _matchVariableModel( |
| chain: null, |
| ofInterest: isEmpty, |
| assigned: true, |
| unassigned: false)); |
| }); |
| |
| test('un-promotes fully', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._declare(h, objectQVar, true) |
| ._tryPromoteForTypeCheck(h, objectQVar, 'int') |
| .ifTrue; |
| expect(s1.promotionInfo.unwrap(h), |
| contains(h.promotionKeyStore.keyForVariable(objectQVar))); |
| var s2 = s1._write( |
| h, |
| _MockNonPromotionReason(), |
| objectQVar, |
| SharedTypeView(Type('int?')), |
| new SsaNode<SharedTypeView<Type>>(null)); |
| expect(s2.reachable.overallReachable, true); |
| expect(s2.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(objectQVar): _matchVariableModel( |
| chain: null, |
| ofInterest: isEmpty, |
| assigned: true, |
| unassigned: false) |
| }); |
| }); |
| |
| test('un-promotes partially, when no exact match', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._declare(h, objectQVar, true) |
| ._tryPromoteForTypeCheck(h, objectQVar, 'num?') |
| .ifTrue |
| ._tryPromoteForTypeCheck(h, objectQVar, 'int') |
| .ifTrue; |
| expect(s1.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(objectQVar): _matchVariableModel( |
| chain: ['num?', 'int'], |
| ofInterest: ['num?', 'int'], |
| assigned: true, |
| unassigned: false) |
| }); |
| var s2 = s1._write( |
| h, |
| _MockNonPromotionReason(), |
| objectQVar, |
| SharedTypeView(Type('num')), |
| new SsaNode<SharedTypeView<Type>>(null)); |
| expect(s2.reachable.overallReachable, true); |
| expect(s2.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(objectQVar): _matchVariableModel( |
| chain: ['num?', 'num'], |
| ofInterest: ['num?', 'int'], |
| assigned: true, |
| unassigned: false) |
| }); |
| }); |
| |
| test('un-promotes partially, when exact match', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._declare(h, objectQVar, true) |
| ._tryPromoteForTypeCheck(h, objectQVar, 'num?') |
| .ifTrue |
| ._tryPromoteForTypeCheck(h, objectQVar, 'num') |
| .ifTrue |
| ._tryPromoteForTypeCheck(h, objectQVar, 'int') |
| .ifTrue; |
| expect(s1.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(objectQVar): _matchVariableModel( |
| chain: ['num?', 'num', 'int'], |
| ofInterest: ['num?', 'num', 'int'], |
| assigned: true, |
| unassigned: false) |
| }); |
| var s2 = s1._write( |
| h, |
| _MockNonPromotionReason(), |
| objectQVar, |
| SharedTypeView(Type('num')), |
| new SsaNode<SharedTypeView<Type>>(null)); |
| expect(s2.reachable.overallReachable, true); |
| expect(s2.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(objectQVar): _matchVariableModel( |
| chain: ['num?', 'num'], |
| ofInterest: ['num?', 'num', 'int'], |
| assigned: true, |
| unassigned: false) |
| }); |
| }); |
| |
| test('leaves promoted, when exact match', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._declare(h, objectQVar, true) |
| ._tryPromoteForTypeCheck(h, objectQVar, 'num?') |
| .ifTrue |
| ._tryPromoteForTypeCheck(h, objectQVar, 'num') |
| .ifTrue; |
| expect(s1.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(objectQVar): _matchVariableModel( |
| chain: ['num?', 'num'], |
| ofInterest: ['num?', 'num'], |
| assigned: true, |
| unassigned: false) |
| }); |
| var s2 = s1._write(h, null, objectQVar, SharedTypeView(Type('num')), |
| new SsaNode<SharedTypeView<Type>>(null)); |
| expect(s2.reachable.overallReachable, true); |
| expect(s2.promotionInfo, isNot(same(s1.promotionInfo))); |
| expect(s2.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(objectQVar): _matchVariableModel( |
| chain: ['num?', 'num'], |
| ofInterest: ['num?', 'num'], |
| assigned: true, |
| unassigned: false) |
| }); |
| }); |
| |
| test('leaves promoted, when writing a subtype', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._declare(h, objectQVar, true) |
| ._tryPromoteForTypeCheck(h, objectQVar, 'num?') |
| .ifTrue |
| ._tryPromoteForTypeCheck(h, objectQVar, 'num') |
| .ifTrue; |
| expect(s1.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(objectQVar): _matchVariableModel( |
| chain: ['num?', 'num'], |
| ofInterest: ['num?', 'num'], |
| assigned: true, |
| unassigned: false) |
| }); |
| var s2 = s1._write(h, null, objectQVar, SharedTypeView(Type('int')), |
| new SsaNode<SharedTypeView<Type>>(null)); |
| expect(s2.reachable.overallReachable, true); |
| expect(s2.promotionInfo, isNot(same(s1.promotionInfo))); |
| expect(s2.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(objectQVar): _matchVariableModel( |
| chain: ['num?', 'num'], |
| ofInterest: ['num?', 'num'], |
| assigned: true, |
| unassigned: false) |
| }); |
| }); |
| |
| group('Promotes to NonNull of a type of interest', () { |
| test('when declared type', () { |
| var x = Var('x')..type = Type('int?'); |
| |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._declare(h, x, true); |
| expect(s1.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(x): |
| _matchVariableModel(chain: null), |
| }); |
| |
| var s2 = s1._write(h, null, x, SharedTypeView(Type('int')), |
| new SsaNode<SharedTypeView<Type>>(null)); |
| expect(s2.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(x): |
| _matchVariableModel(chain: ['int']), |
| }); |
| }); |
| |
| test('when declared type, if write-captured', () { |
| var x = Var('x')..type = Type('int?'); |
| |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._declare(h, x, true); |
| expect(s1.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(x): |
| _matchVariableModel(chain: null), |
| }); |
| |
| var s2 = s1._conservativeJoin(h, [], [x]); |
| expect(s2.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(x): |
| _matchVariableModel(chain: null, writeCaptured: true), |
| }); |
| |
| // 'x' is write-captured, so not promoted |
| var s3 = s2._write(h, null, x, SharedTypeView(Type('int')), |
| new SsaNode<SharedTypeView<Type>>(null)); |
| expect(s3.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(x): |
| _matchVariableModel(chain: null, writeCaptured: true), |
| }); |
| }); |
| |
| test('when promoted', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._declare(h, objectQVar, true) |
| ._tryPromoteForTypeCheck(h, objectQVar, 'int?') |
| .ifTrue; |
| expect(s1.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(objectQVar): _matchVariableModel( |
| chain: ['int?'], |
| ofInterest: ['int?'], |
| ), |
| }); |
| var s2 = s1._write(h, null, objectQVar, SharedTypeView(Type('int')), |
| new SsaNode<SharedTypeView<Type>>(null)); |
| expect(s2.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(objectQVar): _matchVariableModel( |
| chain: ['int?', 'int'], |
| ofInterest: ['int?'], |
| ), |
| }); |
| }); |
| |
| test('when not promoted', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._declare(h, objectQVar, true) |
| ._tryPromoteForTypeCheck(h, objectQVar, 'int?') |
| .ifFalse; |
| expect(s1.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(objectQVar): _matchVariableModel( |
| chain: ['Object'], |
| ofInterest: ['int?'], |
| ), |
| }); |
| var s2 = s1._write(h, null, objectQVar, SharedTypeView(Type('int')), |
| new SsaNode<SharedTypeView<Type>>(null)); |
| expect(s2.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(objectQVar): _matchVariableModel( |
| chain: ['Object', 'int'], |
| ofInterest: ['int?'], |
| ), |
| }); |
| }); |
| }); |
| |
| test('Promotes to type of interest when not previously promoted', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._declare(h, objectQVar, true) |
| ._tryPromoteForTypeCheck(h, objectQVar, 'num?') |
| .ifFalse; |
| expect(s1.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(objectQVar): _matchVariableModel( |
| chain: ['Object'], |
| ofInterest: ['num?'], |
| ), |
| }); |
| var s2 = s1._write( |
| h, |
| _MockNonPromotionReason(), |
| objectQVar, |
| SharedTypeView(Type('num?')), |
| new SsaNode<SharedTypeView<Type>>(null)); |
| expect(s2.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(objectQVar): _matchVariableModel( |
| chain: ['num?'], |
| ofInterest: ['num?'], |
| ), |
| }); |
| }); |
| |
| test('Promotes to type of interest when previously promoted', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._declare(h, objectQVar, true) |
| ._tryPromoteForTypeCheck(h, objectQVar, 'num?') |
| .ifTrue |
| ._tryPromoteForTypeCheck(h, objectQVar, 'int?') |
| .ifFalse; |
| expect(s1.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(objectQVar): _matchVariableModel( |
| chain: ['num?', 'num'], |
| ofInterest: ['num?', 'int?'], |
| ), |
| }); |
| var s2 = s1._write( |
| h, |
| _MockNonPromotionReason(), |
| objectQVar, |
| SharedTypeView(Type('int?')), |
| new SsaNode<SharedTypeView<Type>>(null)); |
| expect(s2.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(objectQVar): _matchVariableModel( |
| chain: ['num?', 'int?'], |
| ofInterest: ['num?', 'int?'], |
| ), |
| }); |
| }); |
| |
| group('Multiple candidate types of interest', () { |
| group('; choose most specific', () { |
| setUp(() { |
| // class A {} |
| // class B extends A {} |
| // class C extends B {} |
| h.addSuperInterfaces( |
| 'C', (_) => [Type('B'), Type('A'), Type('Object')]); |
| h.addSuperInterfaces('B', (_) => [Type('A'), Type('Object')]); |
| h.addSuperInterfaces('A', (_) => [Type('Object')]); |
| }); |
| |
| test('; first', () { |
| var x = Var('x')..type = Type('Object?'); |
| |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._declare(h, x, true) |
| ._tryPromoteForTypeCheck(h, x, 'B?') |
| .ifFalse |
| ._tryPromoteForTypeCheck(h, x, 'A?') |
| .ifFalse; |
| expect(s1.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(x): _matchVariableModel( |
| chain: ['Object'], |
| ofInterest: ['A?', 'B?'], |
| ), |
| }); |
| |
| var s2 = s1._write(h, null, x, SharedTypeView(Type('C')), |
| new SsaNode<SharedTypeView<Type>>(null)); |
| expect(s2.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(x): _matchVariableModel( |
| chain: ['Object', 'B'], |
| ofInterest: ['A?', 'B?'], |
| ), |
| }); |
| }); |
| |
| test('; second', () { |
| var x = Var('x')..type = Type('Object?'); |
| |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._declare(h, x, true) |
| ._tryPromoteForTypeCheck(h, x, 'A?') |
| .ifFalse |
| ._tryPromoteForTypeCheck(h, x, 'B?') |
| .ifFalse; |
| expect(s1.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(x): _matchVariableModel( |
| chain: ['Object'], |
| ofInterest: ['A?', 'B?'], |
| ), |
| }); |
| |
| var s2 = s1._write(h, null, x, SharedTypeView(Type('C')), |
| new SsaNode<SharedTypeView<Type>>(null)); |
| expect(s2.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(x): _matchVariableModel( |
| chain: ['Object', 'B'], |
| ofInterest: ['A?', 'B?'], |
| ), |
| }); |
| }); |
| |
| test('; nullable and non-nullable', () { |
| var x = Var('x')..type = Type('Object?'); |
| |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._declare(h, x, true) |
| ._tryPromoteForTypeCheck(h, x, 'A') |
| .ifFalse |
| ._tryPromoteForTypeCheck(h, x, 'A?') |
| .ifFalse; |
| expect(s1.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(x): _matchVariableModel( |
| chain: ['Object'], |
| ofInterest: ['A', 'A?'], |
| ), |
| }); |
| |
| var s2 = s1._write(h, null, x, SharedTypeView(Type('B')), |
| new SsaNode<SharedTypeView<Type>>(null)); |
| expect(s2.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(x): _matchVariableModel( |
| chain: ['Object', 'A'], |
| ofInterest: ['A', 'A?'], |
| ), |
| }); |
| }); |
| }); |
| |
| group('; ambiguous', () { |
| test('; no promotion', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._declare(h, objectQVar, true) |
| ._tryPromoteForTypeCheck(h, objectQVar, 'num?') |
| .ifFalse |
| ._tryPromoteForTypeCheck(h, objectQVar, 'num*') |
| .ifFalse; |
| expect(s1.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(objectQVar): |
| _matchVariableModel( |
| chain: ['Object'], |
| ofInterest: ['num?', 'num*'], |
| ), |
| }); |
| var s2 = s1._write(h, null, objectQVar, SharedTypeView(Type('int')), |
| new SsaNode<SharedTypeView<Type>>(null)); |
| // It's ambiguous whether to promote to num? or num*, so we don't |
| // promote. |
| expect(s2, isNot(same(s1))); |
| expect(s2.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(objectQVar): |
| _matchVariableModel( |
| chain: ['Object'], |
| ofInterest: ['num?', 'num*'], |
| ), |
| }); |
| }); |
| }); |
| |
| test('exact match', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._declare(h, objectQVar, true) |
| ._tryPromoteForTypeCheck(h, objectQVar, 'num?') |
| .ifFalse |
| ._tryPromoteForTypeCheck(h, objectQVar, 'num*') |
| .ifFalse; |
| expect(s1.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(objectQVar): _matchVariableModel( |
| chain: ['Object'], |
| ofInterest: ['num?', 'num*'], |
| ), |
| }); |
| var s2 = s1._write( |
| h, |
| _MockNonPromotionReason(), |
| objectQVar, |
| SharedTypeView(Type('num?')), |
| new SsaNode<SharedTypeView<Type>>(null)); |
| // It's ambiguous whether to promote to num? or num*, but since the |
| // written type is exactly num?, we use that. |
| expect(s2.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(objectQVar): _matchVariableModel( |
| chain: ['num?'], |
| ofInterest: ['num?', 'num*'], |
| ), |
| }); |
| }); |
| }); |
| }); |
| |
| group('demotion, to NonNull', () { |
| test('when promoted via test', () { |
| var x = Var('x')..type = Type('Object?'); |
| |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._declare(h, x, true) |
| ._tryPromoteForTypeCheck(h, x, 'num?') |
| .ifTrue |
| ._tryPromoteForTypeCheck(h, x, 'int?') |
| .ifTrue; |
| expect(s1.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(x): _matchVariableModel( |
| chain: ['num?', 'int?'], |
| ofInterest: ['num?', 'int?'], |
| ), |
| }); |
| |
| var s2 = s1._write( |
| h, |
| _MockNonPromotionReason(), |
| x, |
| SharedTypeView(Type('double')), |
| new SsaNode<SharedTypeView<Type>>(null)); |
| expect(s2.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(x): _matchVariableModel( |
| chain: ['num?', 'num'], |
| ofInterest: ['num?', 'int?'], |
| ), |
| }); |
| }); |
| }); |
| |
| group('declare', () { |
| var objectQVar = Var('x')..type = Type('Object?'); |
| |
| test('initialized', () { |
| var s = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._declare(h, objectQVar, true); |
| expect(s.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(objectQVar): |
| _matchVariableModel(assigned: true, unassigned: false), |
| }); |
| }); |
| |
| test('not initialized', () { |
| var s = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._declare(h, objectQVar, false); |
| expect(s.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(objectQVar): |
| _matchVariableModel(assigned: false, unassigned: true), |
| }); |
| }); |
| }); |
| |
| group('markNonNullable', () { |
| test('unpromoted -> unchanged', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial); |
| var s2 = s1._tryMarkNonNullable(h, intVar).ifTrue; |
| expect(s2, same(s1)); |
| }); |
| |
| test('unpromoted -> promoted', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial); |
| var s2 = s1._tryMarkNonNullable(h, intQVar).ifTrue; |
| expect(s2.reachable.overallReachable, true); |
| expect(s2._infoFor(h, intQVar), |
| _matchVariableModel(chain: ['int'], ofInterest: [])); |
| }); |
| |
| test('promoted -> unchanged', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._tryPromoteForTypeCheck(h, objectQVar, 'int') |
| .ifTrue; |
| var s2 = s1._tryMarkNonNullable(h, objectQVar).ifTrue; |
| expect(s2, same(s1)); |
| }); |
| |
| test('promoted -> re-promoted', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._tryPromoteForTypeCheck(h, objectQVar, 'int?') |
| .ifTrue; |
| var s2 = s1._tryMarkNonNullable(h, objectQVar).ifTrue; |
| expect(s2.reachable.overallReachable, true); |
| expect(s2.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(objectQVar): |
| _matchVariableModel(chain: ['int?', 'int'], ofInterest: ['int?']) |
| }); |
| }); |
| |
| test('promote to Never', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial); |
| var s2 = s1._tryMarkNonNullable(h, nullVar).ifTrue; |
| expect(s2.reachable.overallReachable, true); |
| expect(s2._infoFor(h, nullVar), |
| _matchVariableModel(chain: ['Never'], ofInterest: [])); |
| }); |
| }); |
| |
| group('conservativeJoin', () { |
| test('unchanged', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._declare(h, intQVar, true) |
| ._tryPromoteForTypeCheck(h, objectQVar, 'int') |
| .ifTrue; |
| var s2 = s1._conservativeJoin(h, [intQVar], []); |
| expect(s2, isNot(same(s1))); |
| expect(s2.reachable, same(s1.reachable)); |
| expect(s2.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(objectQVar): |
| _matchVariableModel(chain: ['int'], ofInterest: ['int']), |
| h.promotionKeyStore.keyForVariable(intQVar): |
| _matchVariableModel(chain: null, ofInterest: []) |
| }); |
| }); |
| |
| test('written', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._tryPromoteForTypeCheck(h, objectQVar, 'int') |
| .ifTrue |
| ._tryPromoteForTypeCheck(h, intQVar, 'int') |
| .ifTrue; |
| var s2 = s1._conservativeJoin(h, [intQVar], []); |
| expect(s2.reachable.overallReachable, true); |
| expect(s2.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(objectQVar): |
| _matchVariableModel(chain: ['int'], ofInterest: ['int']), |
| h.promotionKeyStore.keyForVariable(intQVar): |
| _matchVariableModel(chain: null, ofInterest: ['int']) |
| }); |
| }); |
| |
| test('write captured', () { |
| var s1 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._tryPromoteForTypeCheck(h, objectQVar, 'int') |
| .ifTrue |
| ._tryPromoteForTypeCheck(h, intQVar, 'int') |
| .ifTrue; |
| var s2 = s1._conservativeJoin(h, [], [intQVar]); |
| expect(s2.reachable.overallReachable, true); |
| expect(s2.promotionInfo.unwrap(h), { |
| h.promotionKeyStore.keyForVariable(objectQVar): |
| _matchVariableModel(chain: ['int'], ofInterest: ['int']), |
| h.promotionKeyStore.keyForVariable(intQVar): _matchVariableModel( |
| chain: null, ofInterest: isEmpty, unassigned: false) |
| }); |
| }); |
| }); |
| |
| group('rebaseForward', () { |
| test('reachability', () { |
| var reachable = FlowModel<SharedTypeView<Type>>(Reachability.initial); |
| var unreachable = reachable.setUnreachable(); |
| expect(reachable.rebaseForward(h, reachable), same(reachable)); |
| expect(reachable.rebaseForward(h, unreachable), same(unreachable)); |
| expect( |
| unreachable.rebaseForward(h, reachable).reachable.overallReachable, |
| false); |
| expect(unreachable.rebaseForward(h, reachable).promotionInfo, |
| same(unreachable.promotionInfo)); |
| expect(unreachable.rebaseForward(h, unreachable), same(unreachable)); |
| }); |
| |
| test('assignments', () { |
| var a = Var('a')..type = Type('int'); |
| var b = Var('b')..type = Type('int'); |
| var c = Var('c')..type = Type('int'); |
| var d = Var('d')..type = Type('int'); |
| var s0 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._declare(h, a, false) |
| ._declare(h, b, false) |
| ._declare(h, c, false) |
| ._declare(h, d, false); |
| var s1 = s0 |
| ._write(h, null, a, SharedTypeView(Type('int')), |
| new SsaNode<SharedTypeView<Type>>(null)) |
| ._write(h, null, b, SharedTypeView(Type('int')), |
| new SsaNode<SharedTypeView<Type>>(null)); |
| var s2 = s0 |
| ._write(h, null, a, SharedTypeView(Type('int')), |
| new SsaNode<SharedTypeView<Type>>(null)) |
| ._write(h, null, c, SharedTypeView(Type('int')), |
| new SsaNode<SharedTypeView<Type>>(null)); |
| var result = s1.rebaseForward(h, s2); |
| expect(result._infoFor(h, a).assigned, true); |
| expect(result._infoFor(h, b).assigned, true); |
| expect(result._infoFor(h, c).assigned, true); |
| expect(result._infoFor(h, d).assigned, false); |
| }); |
| |
| test('write captured', () { |
| var a = Var('a')..type = Type('int'); |
| var b = Var('b')..type = Type('int'); |
| var c = Var('c')..type = Type('int'); |
| var d = Var('d')..type = Type('int'); |
| var s0 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._declare(h, a, false) |
| ._declare(h, b, false) |
| ._declare(h, c, false) |
| ._declare(h, d, false); |
| // In s1, a and b are write captured. In s2, a and c are. |
| var s1 = s0._conservativeJoin(h, [a, b], [a, b]); |
| var s2 = s1._conservativeJoin(h, [a, c], [a, c]); |
| var result = s1.rebaseForward(h, s2); |
| expect( |
| result._infoFor(h, a), |
| _matchVariableModel(writeCaptured: true, unassigned: false), |
| ); |
| expect( |
| result._infoFor(h, b), |
| _matchVariableModel(writeCaptured: true, unassigned: false), |
| ); |
| expect( |
| result._infoFor(h, c), |
| _matchVariableModel(writeCaptured: true, unassigned: false), |
| ); |
| expect( |
| result._infoFor(h, d), |
| _matchVariableModel(writeCaptured: false, unassigned: true), |
| ); |
| }); |
| |
| test('write captured and promoted', () { |
| var a = Var('a')..type = Type('num'); |
| var s0 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._declare(h, a, false); |
| // In s1, a is write captured. In s2 it's promoted. |
| var s1 = s0._conservativeJoin(h, [a], [a]); |
| var s2 = s0._tryPromoteForTypeCheck(h, a, 'int').ifTrue; |
| expect( |
| s1.rebaseForward(h, s2)._infoFor(h, a), |
| _matchVariableModel(writeCaptured: true, chain: isNull), |
| ); |
| expect( |
| s2.rebaseForward(h, s1)._infoFor(h, a), |
| _matchVariableModel(writeCaptured: true, chain: isNull), |
| ); |
| }); |
| |
| test('promotion', () { |
| void _check(String? thisType, String? otherType, bool unsafe, |
| List<String>? expectedChain) { |
| var x = Var('x')..type = Type('Object?'); |
| var s0 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._declare(h, x, true); |
| var s1 = s0; |
| if (unsafe) { |
| s1 = s1._write(h, null, x, SharedTypeView(Type('Object?')), |
| new SsaNode<SharedTypeView<Type>>(null)); |
| } |
| if (thisType != null) { |
| s1 = s1._tryPromoteForTypeCheck(h, x, thisType).ifTrue; |
| } |
| var s2 = otherType == null |
| ? s0 |
| : s0._tryPromoteForTypeCheck(h, x, otherType).ifTrue; |
| var result = s2.rebaseForward(h, s1); |
| if (expectedChain == null) { |
| expect(result.promotionInfo.unwrap(h), |
| contains(h.promotionKeyStore.keyForVariable(x))); |
| expect(result._infoFor(h, x).promotedTypes, isNull); |
| } else { |
| expect( |
| result |
| ._infoFor(h, x) |
| .promotedTypes! |
| .map((t) => t.unwrapTypeView().type) |
| .toList(), |
| expectedChain); |
| } |
| } |
| |
| _check(null, null, false, null); |
| _check(null, null, true, null); |
| _check('int', null, false, ['int']); |
| _check('int', null, true, ['int']); |
| _check(null, 'int', false, ['int']); |
| _check(null, 'int', true, null); |
| _check('int?', 'int', false, ['int?', 'int']); |
| _check('int', 'int?', false, ['int']); |
| _check('int', 'String', false, ['int']); |
| _check('int?', 'int', true, ['int?']); |
| _check('int', 'int?', true, ['int']); |
| _check('int', 'String', true, ['int']); |
| }); |
| |
| test('promotion chains', () { |
| // Verify that the given promotion chain matches the expected list of |
| // strings. |
| void _checkChain( |
| List<SharedTypeView<Type>>? chain, List<String> expected) { |
| var strings = (chain ?? <SharedTypeView<Type>>[]) |
| .map((t) => t.unwrapTypeView().type) |
| .toList(); |
| expect(strings, expected); |
| } |
| |
| // Test the following scenario: |
| // - Prior to the try/finally block, the sequence of promotions in |
| // [before] is done. |
| // - During the try block, the sequence of promotions in [inTry] is |
| // done. |
| // - During the finally block, the sequence of promotions in |
| // [inFinally] is done. |
| // - After calling `restrict` to refine the state from the finally |
| // block, the expected promotion chain is [expectedResult]. |
| void _check(List<String> before, List<String> inTry, |
| List<String> inFinally, List<String> expectedResult) { |
| var x = Var('x')..type = Type('Object?'); |
| var initialModel = |
| FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._declare(h, x, true); |
| for (var t in before) { |
| initialModel = initialModel._tryPromoteForTypeCheck(h, x, t).ifTrue; |
| } |
| _checkChain(initialModel._infoFor(h, x).promotedTypes, before); |
| var tryModel = initialModel; |
| for (var t in inTry) { |
| tryModel = tryModel._tryPromoteForTypeCheck(h, x, t).ifTrue; |
| } |
| var expectedTryChain = before.toList()..addAll(inTry); |
| _checkChain(tryModel._infoFor(h, x).promotedTypes, expectedTryChain); |
| var finallyModel = initialModel; |
| for (var t in inFinally) { |
| finallyModel = finallyModel._tryPromoteForTypeCheck(h, x, t).ifTrue; |
| } |
| var expectedFinallyChain = before.toList()..addAll(inFinally); |
| _checkChain( |
| finallyModel._infoFor(h, x).promotedTypes, expectedFinallyChain); |
| var result = tryModel.rebaseForward(h, finallyModel); |
| _checkChain(result._infoFor(h, x).promotedTypes, expectedResult); |
| // And verify that the inputs are unchanged. |
| _checkChain(initialModel._infoFor(h, x).promotedTypes, before); |
| _checkChain(tryModel._infoFor(h, x).promotedTypes, expectedTryChain); |
| _checkChain( |
| finallyModel._infoFor(h, x).promotedTypes, expectedFinallyChain); |
| } |
| |
| _check( |
| ['Object'], |
| ['num', 'int'], |
| ['Iterable<dynamic>', 'List<dynamic>'], |
| ['Object', 'Iterable<dynamic>', 'List<dynamic>']); |
| _check([], ['num', 'int'], ['Iterable<dynamic>', 'List<dynamic>'], |
| ['Iterable<dynamic>', 'List<dynamic>']); |
| _check(['Object'], [], ['Iterable<dynamic>', 'List<dynamic>'], |
| ['Object', 'Iterable<dynamic>', 'List<dynamic>']); |
| _check([], [], ['Iterable<dynamic>', 'List<dynamic>'], |
| ['Iterable<dynamic>', 'List<dynamic>']); |
| _check(['Object'], ['num', 'int'], [], ['Object', 'num', 'int']); |
| _check([], ['num', 'int'], [], ['num', 'int']); |
| _check(['Object'], [], [], ['Object']); |
| _check([], [], [], []); |
| _check([], ['num', 'int'], ['Object', 'Iterable<dynamic>'], |
| ['Object', 'Iterable<dynamic>']); |
| _check([], ['num', 'int'], ['Object'], ['Object', 'num', 'int']); |
| _check([], ['Object', 'Iterable<dynamic>'], ['num', 'int'], |
| ['num', 'int']); |
| _check([], ['Object'], ['num', 'int'], ['num', 'int']); |
| _check([], ['num'], ['Object', 'int'], ['Object', 'int']); |
| _check([], ['int'], ['Object', 'num'], ['Object', 'num', 'int']); |
| _check([], ['Object', 'int'], ['num'], ['num', 'int']); |
| _check([], ['Object', 'num'], ['int'], ['int']); |
| }); |
| |
| test('types of interest', () { |
| var a = Var('a')..type = Type('Object'); |
| var s0 = FlowModel<SharedTypeView<Type>>(Reachability.initial) |
| ._declare(h, a, false); |
| var s1 = s0._tryPromoteForTypeCheck(h, a, 'int').ifFalse; |
| var s2 = s0._tryPromoteForTypeCheck(h, a, 'String').ifFalse; |
| expect( |
| s1.rebaseForward(h, s2)._infoFor(h, a), |
| _matchVariableModel(ofInterest: ['int', 'String']), |
| ); |
| expect( |
| s2.rebaseForward(h, s1)._infoFor(h, a), |
| _matchVariableModel(ofInterest: ['int', 'String']), |
| ); |
| }); |
| |
| test('variable present in one state but not the other', () { |
| var x = Var('x')..type = Type('Object?'); |
| var s0 = FlowModel<SharedTypeView<Type>>(Reachability.initial); |
| var s1 = s0._declare(h, x, true); |
| expect(s1.rebaseForward(h, s0), same(s1)); |
| expect(s0.rebaseForward(h, s1), same(s1)); |
| }); |
| }); |
| }); |
| |
| group('joinPromotionChains', () { |
| var doubleType = Type('double'); |
| var intType = Type('int'); |
| var numType = Type('num'); |
| var objectType = Type('Object'); |
| |
| test('should handle nulls', () { |
| expect( |
| PromotionModel.joinPromotedTypes(null, null, h.typeOperations), null); |
| expect( |
| PromotionModel.joinPromotedTypes(null, [intType], h.typeOperations), |
| null); |
| expect( |
| PromotionModel.joinPromotedTypes([intType], null, h.typeOperations), |
| null); |
| }); |
| |
| test('should return null if there are no common types', () { |
| expect( |
| PromotionModel.joinPromotedTypes( |
| [intType], [doubleType], h.typeOperations), |
| null); |
| }); |
| |
| test('should return common prefix if there are common types', () { |
| expect( |
| PromotionModel.joinPromotedTypes( |
| [SharedTypeView(objectType), SharedTypeView(intType)], |
| [SharedTypeView(objectType), SharedTypeView(doubleType)], |
| h.typeOperations), |
| _matchPromotionChain(['Object'])); |
| expect( |
| PromotionModel.joinPromotedTypes([ |
| SharedTypeView(objectType), |
| SharedTypeView(numType), |
| SharedTypeView(intType) |
| ], [ |
| SharedTypeView(objectType), |
| SharedTypeView(numType), |
| SharedTypeView(doubleType) |
| ], h.typeOperations), |
| _matchPromotionChain(['Object', 'num'])); |
| }); |
| |
| test('should return an input if it is a prefix of the other', () { |
| var prefix = [objectType, numType]; |
| var largerChain = [objectType, numType, intType]; |
| expect( |
| PromotionModel.joinPromotedTypes( |
| prefix, largerChain, h.typeOperations), |
| same(prefix)); |
| expect( |
| PromotionModel.joinPromotedTypes( |
| largerChain, prefix, h.typeOperations), |
| same(prefix)); |
| expect(PromotionModel.joinPromotedTypes(prefix, prefix, h.typeOperations), |
| same(prefix)); |
| }); |
| |
| test('should intersect', () { |
| // F <: E <: D <: C <: B <: A |
| var A = Type('A'); |
| var B = Type('B'); |
| var C = Type('C'); |
| var D = Type('D'); |
| var E = Type('E'); |
| var F = Type('F'); |
| h.addSuperInterfaces( |
| 'F', |
| (_) => [ |
| Type('E'), |
| Type('D'), |
| Type('C'), |
| Type('B'), |
| Type('A'), |
| Type('Object') |
| ]); |
| h.addSuperInterfaces('E', |
| (_) => [Type('D'), Type('C'), Type('B'), Type('A'), Type('Object')]); |
| h.addSuperInterfaces( |
| 'D', (_) => [Type('C'), Type('B'), Type('A'), Type('Object')]); |
| h.addSuperInterfaces('C', (_) => [Type('B'), Type('A'), Type('Object')]); |
| h.addSuperInterfaces('B', (_) => [Type('A'), Type('Object')]); |
| h.addSuperInterfaces('A', (_) => [Type('Object')]); |
| |
| void check(List<SharedTypeView<Type>> chain1, |
| List<SharedTypeView<Type>> chain2, Matcher matcher) { |
| expect( |
| PromotionModel.joinPromotedTypes(chain1, chain2, h.typeOperations), |
| matcher, |
| ); |
| |
| expect( |
| PromotionModel.joinPromotedTypes(chain2, chain1, h.typeOperations), |
| matcher, |
| ); |
| } |
| |
| { |
| var chain1 = [SharedTypeView(A), SharedTypeView(B), SharedTypeView(C)]; |
| var chain2 = [SharedTypeView(A), SharedTypeView(C)]; |
| check(chain1, chain2, same(chain2)); |
| } |
| |
| check( |
| [ |
| SharedTypeView(A), |
| SharedTypeView(B), |
| SharedTypeView(C), |
| SharedTypeView(F) |
| ], |
| [ |
| SharedTypeView(A), |
| SharedTypeView(D), |
| SharedTypeView(E), |
| SharedTypeView(F) |
| ], |
| _matchPromotionChain(['A', 'F']), |
| ); |
| |
| check( |
| [ |
| SharedTypeView(A), |
| SharedTypeView(B), |
| SharedTypeView(E), |
| SharedTypeView(F) |
| ], |
| [ |
| SharedTypeView(A), |
| SharedTypeView(C), |
| SharedTypeView(D), |
| SharedTypeView(F) |
| ], |
| _matchPromotionChain(['A', 'F']), |
| ); |
| |
| check( |
| [SharedTypeView(A), SharedTypeView(C), SharedTypeView(E)], |
| [SharedTypeView(B), SharedTypeView(C), SharedTypeView(D)], |
| _matchPromotionChain(['C']), |
| ); |
| |
| check( |
| [ |
| SharedTypeView(A), |
| SharedTypeView(C), |
| SharedTypeView(E), |
| SharedTypeView(F) |
| ], |
| [ |
| SharedTypeView(B), |
| SharedTypeView(C), |
| SharedTypeView(D), |
| SharedTypeView(F) |
| ], |
| _matchPromotionChain(['C', 'F']), |
| ); |
| |
| check( |
| [SharedTypeView(A), SharedTypeView(B), SharedTypeView(C)], |
| [SharedTypeView(A), SharedTypeView(B), SharedTypeView(D)], |
| _matchPromotionChain(['A', 'B']), |
| ); |
| }); |
| }); |
| |
| group('joinTypesOfInterest', () { |
| List<Type> _makeTypes(List<String> typeNames) => |
| typeNames.map((t) => Type(t)).toList(); |
| |
| test('simple prefix', () { |
| var s1 = _makeTypes(['double', 'int']); |
| var s2 = _makeTypes(['double', 'int', 'bool']); |
| var expected = _matchOfInterestSet(['double', 'int', 'bool']); |
| expect(PromotionModel.joinTested(s1, s2, h.typeOperations), expected); |
| expect(PromotionModel.joinTested(s2, s1, h.typeOperations), expected); |
| }); |
| |
| test('common prefix', () { |
| var s1 = _makeTypes(['double', 'int', 'String']); |
| var s2 = _makeTypes(['double', 'int', 'bool']); |
| var expected = _matchOfInterestSet(['double', 'int', 'String', 'bool']); |
| expect(PromotionModel.joinTested(s1, s2, h.typeOperations), expected); |
| expect(PromotionModel.joinTested(s2, s1, h.typeOperations), expected); |
| }); |
| |
| test('order mismatch', () { |
| var s1 = _makeTypes(['double', 'int']); |
| var s2 = _makeTypes(['int', 'double']); |
| var expected = _matchOfInterestSet(['double', 'int']); |
| expect(PromotionModel.joinTested(s1, s2, h.typeOperations), expected); |
| expect(PromotionModel.joinTested(s2, s1, h.typeOperations), expected); |
| }); |
| |
| test('small common prefix', () { |
| var s1 = _makeTypes(['int', 'double', 'String', 'bool']); |
| var s2 = _makeTypes(['int', 'List', 'bool', 'Future']); |
| var expected = _matchOfInterestSet( |
| ['int', 'double', 'String', 'bool', 'List', 'Future']); |
| expect(PromotionModel.joinTested(s1, s2, h.typeOperations), expected); |
| expect(PromotionModel.joinTested(s2, s1, h.typeOperations), expected); |
| }); |
| }); |
| |
| group('join', () { |
| late int x; |
| late int y; |
| late int z; |
| late int w; |
| var intType = Type('int'); |
| var intQType = Type('int?'); |
| var stringType = Type('String'); |
| |
| setUp(() { |
| x = h.promotionKeyStore.keyForVariable(Var('x')..type = Type('Object?')); |
| y = h.promotionKeyStore.keyForVariable(Var('y')..type = Type('Object?')); |
| z = h.promotionKeyStore.keyForVariable(Var('z')..type = Type('Object?')); |
| w = h.promotionKeyStore.keyForVariable(Var('w')..type = Type('Object?')); |
| }); |
| |
| PromotionModel<SharedTypeView<Type>> model( |
| List<SharedTypeView<Type>>? promotionChain, |
| {List<SharedTypeView<Type>>? typesOfInterest, |
| bool assigned = false}) => |
| PromotionModel<SharedTypeView<Type>>( |
| promotedTypes: promotionChain, |
| tested: typesOfInterest ?? promotionChain ?? [], |
| assigned: assigned, |
| unassigned: !assigned, |
| ssaNode: new SsaNode<SharedTypeView<Type>>(null)); |
| |
| group('without input reuse', () { |
| test('promoted with unpromoted', () { |
| var s0 = FlowModel<SharedTypeView<Type>>(Reachability.initial); |
| var s1 = s0._setInfo(h, { |
| x: model([SharedTypeView(intType)]), |
| y: model(null) |
| }); |
| var s2 = s0._setInfo(h, { |
| x: model(null), |
| y: model([SharedTypeView(intType)]) |
| }); |
| expect(FlowModel.joinPromotionInfo(h, s1, s2).promotionInfo.unwrap(h), { |
| x: _matchVariableModel(chain: null, ofInterest: ['int']), |
| y: _matchVariableModel(chain: null, ofInterest: ['int']) |
| }); |
| }); |
| }); |
| group('should re-use an input if possible', () { |
| test('identical inputs', () { |
| var s0 = FlowModel<SharedTypeView<Type>>(Reachability.initial); |
| var s1 = s0._setInfo(h, { |
| x: model([SharedTypeView(intType)]), |
| y: model([SharedTypeView(stringType)]) |
| }); |
| expect(FlowModel.joinPromotionInfo(h, s1, s1), same(s1)); |
| }); |
| |
| test('one input empty', () { |
| var s0 = FlowModel<SharedTypeView<Type>>(Reachability.initial); |
| var s1 = s0._setInfo(h, { |
| x: model([SharedTypeView(intType)]), |
| y: model([SharedTypeView(stringType)]) |
| }); |
| var s2 = s0; |
| const Null expected = null; |
| expect(FlowModel.joinPromotionInfo(h, s1, s2).promotionInfo, |
| same(expected)); |
| expect(FlowModel.joinPromotionInfo(h, s2, s1).promotionInfo, |
| same(expected)); |
| }); |
| |
| test('promoted with unpromoted', () { |
| var s0 = FlowModel<SharedTypeView<Type>>(Reachability.initial); |
| var s1 = s0._setInfo(h, { |
| x: model([SharedTypeView(intType)]) |
| }); |
| var s2 = s0._setInfo(h, {x: model(null)}); |
| var expected = { |
| x: _matchVariableModel(chain: null, ofInterest: ['int']) |
| }; |
| expect(FlowModel.joinPromotionInfo(h, s1, s2).promotionInfo.unwrap(h), |
| expected); |
| expect(FlowModel.joinPromotionInfo(h, s2, s1).promotionInfo.unwrap(h), |
| expected); |
| }); |
| |
| test('related type chains', () { |
| var s0 = FlowModel<SharedTypeView<Type>>(Reachability.initial); |
| var s1 = s0._setInfo(h, { |
| x: model([SharedTypeView(intQType), SharedTypeView(intType)]) |
| }); |
| var s2 = s0._setInfo(h, { |
| x: model([SharedTypeView(intQType)]) |
| }); |
| var expected = { |
| x: _matchVariableModel(chain: ['int?'], ofInterest: ['int?', 'int']) |
| }; |
| expect(FlowModel.joinPromotionInfo(h, s1, s2).promotionInfo.unwrap(h), |
| expected); |
| expect(FlowModel.joinPromotionInfo(h, s2, s1).promotionInfo.unwrap(h), |
| expected); |
| }); |
| |
| test('unrelated type chains', () { |
| var s0 = FlowModel<SharedTypeView<Type>>(Reachability.initial); |
| var s1 = s0._setInfo(h, { |
| x: model([SharedTypeView(intType)]) |
| }); |
| var s2 = s0._setInfo(h, { |
| x: model([SharedTypeView(stringType)]) |
| }); |
| var expected = { |
| x: _matchVariableModel(chain: null, ofInterest: ['String', 'int']) |
| }; |
| expect(FlowModel.joinPromotionInfo(h, s1, s2).promotionInfo.unwrap(h), |
| expected); |
| expect(FlowModel.joinPromotionInfo(h, s2, s1).promotionInfo.unwrap(h), |
| expected); |
| }); |
| |
| test('sub-map', () { |
| var s0 = FlowModel<SharedTypeView<Type>>(Reachability.initial); |
| var xModel = model([SharedTypeView(intType)]); |
| var s1 = s0._setInfo(h, { |
| x: xModel, |
| y: model([SharedTypeView(stringType)]) |
| }); |
| var s2 = s0._setInfo(h, {x: xModel}); |
| var expected = {x: xModel}; |
| expect(FlowModel.joinPromotionInfo(h, s1, s2).promotionInfo.unwrap(h), |
| expected); |
| expect(FlowModel.joinPromotionInfo(h, s2, s1).promotionInfo.unwrap(h), |
| expected); |
| }); |
| |
| test('sub-map with matched subtype', () { |
| var s0 = FlowModel<SharedTypeView<Type>>(Reachability.initial); |
| var s1 = s0._setInfo(h, { |
| x: model([SharedTypeView(intQType), SharedTypeView(intType)]), |
| y: model([SharedTypeView(stringType)]) |
| }); |
| var s2 = s0._setInfo(h, { |
| x: model([SharedTypeView(intQType)]) |
| }); |
| var expected = { |
| x: _matchVariableModel(chain: ['int?'], ofInterest: ['int?', 'int']) |
| }; |
| expect(FlowModel.joinPromotionInfo(h, s1, s2).promotionInfo.unwrap(h), |
| expected); |
| expect(FlowModel.joinPromotionInfo(h, s2, s1).promotionInfo.unwrap(h), |
| expected); |
| }); |
| |
| test('sub-map with mismatched subtype', () { |
| var s0 = FlowModel<SharedTypeView<Type>>(Reachability.initial); |
| var s1 = s0._setInfo(h, { |
| x: model([SharedTypeView(intQType)]), |
| y: model([SharedTypeView(stringType)]) |
| }); |
| var s2 = s0._setInfo(h, { |
| x: model([SharedTypeView(intQType), SharedTypeView(intType)]) |
| }); |
| var expected = { |
| x: _matchVariableModel(chain: ['int?'], ofInterest: ['int?', 'int']) |
| }; |
| expect(FlowModel.joinPromotionInfo(h, s1, s2).promotionInfo.unwrap(h), |
| expected); |
| expect(FlowModel.joinPromotionInfo(h, s2, s1).promotionInfo.unwrap(h), |
| expected); |
| }); |
| |
| test('assigned', () { |
| var s0 = FlowModel<SharedTypeView<Type>>(Reachability.initial); |
| var unassigned = model(null, assigned: false); |
| var assigned = model(null, assigned: true); |
| var s1 = s0._setInfo( |
| h, {x: assigned, y: assigned, z: unassigned, w: unassigned}); |
| var s2 = s0._setInfo( |
| h, {x: assigned, y: unassigned, z: assigned, w: unassigned}); |
| var joined = FlowModel.joinPromotionInfo(h, s1, s2); |
| expect(joined.promotionInfo.unwrap(h), { |
| x: same(assigned), |
| y: _matchVariableModel( |
| chain: null, assigned: false, unassigned: false), |
| z: _matchVariableModel( |
| chain: null, assigned: false, unassigned: false), |
| w: same(unassigned) |
| }); |
| }); |
| |
| test('write captured', () { |
| var s0 = FlowModel<SharedTypeView<Type>>(Reachability.initial); |
| var intQModel = model([SharedTypeView(intQType)]); |
| var writeCapturedModel = intQModel.writeCapture(); |
| var s1 = s0._setInfo(h, { |
| x: writeCapturedModel, |
| y: writeCapturedModel, |
| z: intQModel, |
| w: intQModel |
| }); |
| var s2 = s0._setInfo(h, { |
| x: writeCapturedModel, |
| y: intQModel, |
| z: writeCapturedModel, |
| w: intQModel |
| }); |
| var joined = FlowModel.joinPromotionInfo(h, s1, s2); |
| expect(joined.promotionInfo.unwrap(h), { |
| x: same(writeCapturedModel), |
| y: same(writeCapturedModel), |
| z: same(writeCapturedModel), |
| w: same(intQModel) |
| }); |
| }); |
| }); |
| }); |
| |
| group('inheritTested', () { |
| late int x; |
| var intType = Type('int'); |
| var stringType = Type('String'); |
| |
| setUp(() { |
| x = h.promotionKeyStore.keyForVariable(Var('x')..type = Type('Object?')); |
| }); |
| |
| PromotionModel<SharedTypeView<Type>> model( |
| List<SharedTypeView<Type>> typesOfInterest) => |
| PromotionModel<SharedTypeView<Type>>( |
| promotedTypes: null, |
| tested: typesOfInterest, |
| assigned: true, |
| unassigned: false, |
| ssaNode: new SsaNode<SharedTypeView<Type>>(null)); |
| |
| test('inherits types of interest from other', () { |
| var m0 = FlowModel<SharedTypeView<Type>>(Reachability.initial); |
| var m1 = m0._setInfo(h, { |
| x: model([SharedTypeView(intType)]) |
| }); |
| var m2 = m0._setInfo(h, { |
| x: model([SharedTypeView(stringType)]) |
| }); |
| expect(m1.inheritTested(h, m2).promotionInfo!.get(h, x)!.tested, |
| _matchOfInterestSet(['int', 'String'])); |
| }); |
| |
| test('handles variable missing from other', () { |
| var m0 = FlowModel<SharedTypeView<Type>>(Reachability.initial); |
| var m1 = m0._setInfo(h, { |
| x: model([SharedTypeView(intType)]) |
| }); |
| var m2 = m0; |
| expect(m1.inheritTested(h, m2), same(m1)); |
| }); |
| |
| test('returns identical model when no changes', () { |
| var m0 = FlowModel<SharedTypeView<Type>>(Reachability.initial); |
| var m1 = m0._setInfo(h, { |
| x: model([SharedTypeView(intType)]) |
| }); |
| var m2 = m0._setInfo(h, { |
| x: model([SharedTypeView(intType)]) |
| }); |
| expect(m1.inheritTested(h, m2), same(m1)); |
| }); |
| }); |
| |
| group('Legacy promotion', () { |
| group('if statement', () { |
| group('promotes a variable whose type is shown by its condition', () { |
| test('within then-block', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| if_(x.is_('int'), [ |
| checkPromoted(x, 'int'), |
| ]), |
| ]); |
| }); |
| |
| test('but not within else-block', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| if_(x.is_('int'), [], [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| |
| test('unless the then-block mutates it', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| if_(x.is_('int'), [ |
| checkNotPromoted(x), |
| x.write(expr('int')), |
| ]), |
| ]); |
| }); |
| |
| test('even if the condition mutates it', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| if_( |
| x |
| .write(expr('int')) |
| .parenthesized |
| .eq(expr('int')) |
| .and(x.is_('int')), |
| [ |
| checkPromoted(x, 'int'), |
| ]), |
| ]); |
| }); |
| |
| test('even if the else-block mutates it', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| if_(x.is_('int'), [ |
| checkPromoted(x, 'int'), |
| ], [ |
| x.write(expr('int')), |
| ]), |
| ]); |
| }); |
| |
| test('unless a closure mutates it', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| if_(x.is_('int'), [ |
| checkNotPromoted(x), |
| ]), |
| localFunction([ |
| x.write(expr('int')), |
| ]), |
| ]); |
| }); |
| |
| test( |
| 'unless a closure in the then-block accesses it and it is mutated ' |
| 'anywhere', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| if_(x.is_('int'), [ |
| checkNotPromoted(x), |
| localFunction([ |
| x, |
| ]), |
| ]), |
| x.write(expr('int')), |
| ]); |
| }); |
| |
| test( |
| 'unless a closure in the then-block accesses it and it is mutated ' |
| 'anywhere, even if the access is deeply nested', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| if_(x.is_('int'), [ |
| checkNotPromoted(x), |
| localFunction([ |
| localFunction([ |
| x, |
| ]), |
| ]), |
| ]), |
| x.write(expr('int')), |
| ]); |
| }); |
| |
| test( |
| 'even if a closure in the condition accesses it and it is mutated ' |
| 'somewhere', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| if_( |
| second( |
| localFunction([ |
| x, |
| ]), |
| expr('bool')) |
| .and(x.is_('int')), |
| [ |
| checkPromoted(x, 'int'), |
| ]), |
| x.write(expr('int')), |
| ]); |
| }); |
| |
| test( |
| 'even if a closure in the else-block accesses it and it is mutated ' |
| 'somewhere', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| if_(x.is_('int'), [ |
| checkPromoted(x, 'int'), |
| ], [ |
| localFunction([ |
| x, |
| ]), |
| ]), |
| x.write(expr('int')), |
| ]); |
| }); |
| |
| test( |
| 'even if a closure in the then-block accesses it, provided it is ' |
| 'not mutated anywhere', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| if_(x.is_('int'), [ |
| checkPromoted(x, 'int'), |
| localFunction([ |
| x, |
| ]), |
| ]), |
| ]); |
| }); |
| }); |
| |
| test('handles arbitrary conditions', () { |
| h.enableLegacy(); |
| h.run([ |
| if_(expr('bool'), []), |
| ]); |
| }); |
| |
| test('handles a condition that is a variable', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'bool'), |
| if_(x, []), |
| ]); |
| }); |
| |
| test('handles multiple promotions', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(x, type: 'Object'), |
| declare(y, type: 'Object'), |
| if_(x.is_('int').and(y.is_('String')), [ |
| checkPromoted(x, 'int'), |
| checkPromoted(y, 'String'), |
| ]), |
| ]); |
| }); |
| }); |
| |
| group('conditional expression', () { |
| group('promotes a variable whose type is shown by its condition', () { |
| test('within then-expression', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| x.is_('int').conditional( |
| second(checkPromoted(x, 'int'), expr('Object')), |
| expr('Object')), |
| ]); |
| }); |
| |
| test('but not within else-expression', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| x.is_('int').conditional( |
| expr('Object'), second(checkNotPromoted(x), expr('Object'))), |
| ]); |
| }); |
| |
| test('unless the then-expression mutates it', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| x.is_('int').conditional( |
| second( |
| listLiteral(elementType: 'dynamic', [ |
| checkNotPromoted(x), |
| x.write(expr('int')), |
| ]), |
| expr('Object')), |
| expr('Object')), |
| ]); |
| }); |
| |
| test('even if the condition mutates it', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| x |
| .write(expr('int')) |
| .parenthesized |
| .eq(expr('int')) |
| .and(x.is_('int')) |
| .conditional(second(checkPromoted(x, 'int'), expr('Object')), |
| expr('Object')), |
| ]); |
| }); |
| |
| test('even if the else-expression mutates it', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| x.is_('int').conditional( |
| second(checkPromoted(x, 'int'), expr('int')), |
| x.write(expr('int'))), |
| ]); |
| }); |
| |
| test('unless a closure mutates it', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| x.is_('int').conditional( |
| second(checkNotPromoted(x), expr('Object')), expr('Object')), |
| localFunction([ |
| x.write(expr('int')), |
| ]), |
| ]); |
| }); |
| |
| test( |
| 'unless a closure in the then-expression accesses it and it is ' |
| 'mutated anywhere', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| x.is_('int').conditional( |
| second( |
| listLiteral(elementType: 'dynamic', [ |
| checkNotPromoted(x), |
| localFunction([ |
| x, |
| ]), |
| ]), |
| expr('Object')), |
| expr('Object')), |
| x.write(expr('int')), |
| ]); |
| }); |
| |
| test( |
| 'even if a closure in the condition accesses it and it is mutated ' |
| 'somewhere', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| second( |
| localFunction([ |
| x, |
| ]), |
| expr('Object')) |
| .and(x.is_('int')) |
| .conditional(second(checkPromoted(x, 'int'), expr('Object')), |
| expr('Object')), |
| x.write(expr('int')), |
| ]); |
| }); |
| |
| test( |
| 'even if a closure in the else-expression accesses it and it is ' |
| 'mutated somewhere', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| x.is_('int').conditional( |
| second(checkPromoted(x, 'int'), expr('Object')), |
| second( |
| localFunction([ |
| x, |
| ]), |
| expr('Object'))), |
| x.write(expr('int')), |
| ]); |
| }); |
| |
| test( |
| 'even if a closure in the then-expression accesses it, provided it ' |
| 'is not mutated anywhere', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| x.is_('int').conditional( |
| second( |
| listLiteral(elementType: 'dynamic', [ |
| checkPromoted(x, 'int'), |
| localFunction([ |
| x, |
| ]), |
| ]), |
| expr('Object')), |
| expr('Object')), |
| ]); |
| }); |
| }); |
| |
| test('handles arbitrary conditions', () { |
| h.enableLegacy(); |
| h.run([ |
| expr('bool').conditional(expr('Object'), expr('Object')), |
| ]); |
| }); |
| |
| test('handles a condition that is a variable', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'bool'), |
| x.conditional(expr('Object'), expr('Object')), |
| ]); |
| }); |
| |
| test('handles multiple promotions', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(x, type: 'Object'), |
| declare(y, type: 'Object'), |
| x.is_('int').and(y.is_('String')).conditional( |
| second( |
| listLiteral(elementType: 'dynamic', [ |
| checkPromoted(x, 'int'), |
| checkPromoted(y, 'String'), |
| ]), |
| expr('Object')), |
| expr('Object')) |
| ]); |
| }); |
| }); |
| |
| group('logical', () { |
| group('and', () { |
| group("shows a variable's type", () { |
| test('if the lhs shows the type', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| if_(x.is_('int').and(expr('bool')), [ |
| checkPromoted(x, 'int'), |
| ]), |
| ]); |
| }); |
| |
| test('if the rhs shows the type', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| if_(expr('bool').and(x.is_('int')), [ |
| checkPromoted(x, 'int'), |
| ]), |
| ]); |
| }); |
| |
| test('unless the rhs mutates it', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| if_(x.is_('int').and(x.write(expr('bool'))), [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| |
| test('unless the rhs mutates it, even if the rhs also shows the type', |
| () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| if_( |
| expr('bool').and( |
| x.write(expr('Object')).and(x.is_('int')).parenthesized), |
| [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| |
| test('unless a closure mutates it', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| if_(x.is_('int').and(expr('bool')), [ |
| checkNotPromoted(x), |
| ]), |
| localFunction([ |
| x.write(expr('int')), |
| ]), |
| ]); |
| }); |
| }); |
| |
| group('promotes a variable whose type is shown by its lhs', () { |
| test('within its rhs', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| x.is_('int').and(second(checkPromoted(x, 'int'), expr('bool'))), |
| ]); |
| }); |
| |
| test('unless the lhs mutates it', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| x |
| .write(expr('int')) |
| .parenthesized |
| .eq(expr('int')) |
| .and(x.is_('int')) |
| .parenthesized |
| .and(second(checkNotPromoted(x), expr('bool'))), |
| ]); |
| }); |
| |
| test('unless the rhs mutates it', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| x |
| .is_('int') |
| .and(second(checkNotPromoted(x), x.write(expr('bool')))), |
| ]); |
| }); |
| |
| test('unless a closure mutates it', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| x.is_('int').and(second(checkNotPromoted(x), expr('bool'))), |
| localFunction([ |
| x.write(expr('int')), |
| ]), |
| ]); |
| }); |
| |
| test( |
| 'unless a closure in the rhs accesses it and it is mutated ' |
| 'anywhere', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| x.is_('int').and(second( |
| listLiteral(elementType: 'dynamic', [ |
| checkNotPromoted(x), |
| localFunction([ |
| x, |
| ]), |
| ]), |
| expr('bool'))), |
| x.write(expr('int')), |
| ]); |
| }); |
| |
| test( |
| 'even if a closure in the lhs accesses it and it is mutated ' |
| 'somewhere', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| second( |
| localFunction([ |
| x, |
| ]), |
| expr('Object')) |
| .and(x.is_('int')) |
| .parenthesized |
| .and(second(checkPromoted(x, 'int'), expr('bool'))), |
| x.write(expr('int')), |
| ]); |
| }); |
| |
| test( |
| 'even if a closure in the rhs accesses it, provided it is not ' |
| 'mutated anywhere', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| x.is_('int').and(second( |
| listLiteral(elementType: 'dynamic', [ |
| checkPromoted(x, 'int'), |
| localFunction([ |
| x, |
| ]), |
| ]), |
| expr('bool'))), |
| ]); |
| }); |
| }); |
| |
| test('uses lhs promotion if rhs is not to a subtype', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| // Note: for this to be an effective test, we need to mutate `x` on |
| // the LHS of the outer `&&` so that `x` is not promoted on the RHS |
| // (and thus the lesser promotion on the RHS can take effect). |
| h.run([ |
| declare(x, type: 'Object'), |
| if_( |
| x |
| .write(expr('Object')) |
| .parenthesized |
| .and(x.is_('int')) |
| .parenthesized |
| .and(x.is_('num')), |
| [ |
| checkPromoted(x, 'int'), |
| ]), |
| ]); |
| }); |
| |
| test('uses rhs promotion if rhs is to a subtype', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| if_(x.is_('num').and(x.is_('int')), [ |
| checkPromoted(x, 'int'), |
| ]), |
| ]); |
| }); |
| |
| test('can handle multiple promotions on lhs', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(x, type: 'Object'), |
| declare(y, type: 'Object'), |
| x.is_('int').and(y.is_('String')).parenthesized.and(second( |
| listLiteral(elementType: 'dynamic', [ |
| checkPromoted(x, 'int'), |
| checkPromoted(y, 'String'), |
| ]), |
| expr('bool'))), |
| ]); |
| }); |
| |
| test('handles variables', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(x, type: 'bool'), |
| declare(y, type: 'bool'), |
| if_(x.and(y), []), |
| ]); |
| }); |
| |
| test('handles arbitrary expressions', () { |
| h.enableLegacy(); |
| h.run([ |
| if_(expr('bool').and(expr('bool')), []), |
| ]); |
| }); |
| }); |
| |
| test('or is ignored', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| if_(x.is_('int').or(x.is_('int')), [ |
| checkNotPromoted(x), |
| ], [ |
| checkNotPromoted(x), |
| ]) |
| ]); |
| }); |
| }); |
| |
| group('is test', () { |
| group("shows a variable's type", () { |
| test('normally', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| if_(x.is_('int'), [ |
| checkPromoted(x, 'int'), |
| ], [ |
| checkNotPromoted(x), |
| ]) |
| ]); |
| }); |
| |
| test('unless the test is inverted', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| if_(x.is_('int', isInverted: true), [ |
| checkNotPromoted(x), |
| ], [ |
| checkNotPromoted(x), |
| ]) |
| ]); |
| }); |
| |
| test('unless the tested type is not a subtype of the declared type', |
| () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'String'), |
| if_(x.is_('int'), [ |
| checkNotPromoted(x), |
| ], [ |
| checkNotPromoted(x), |
| ]) |
| ]); |
| }); |
| |
| test("even when the variable's type has been previously promoted", () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| if_(x.is_('num'), [ |
| if_(x.is_('int'), [ |
| checkPromoted(x, 'int'), |
| ], [ |
| checkPromoted(x, 'num'), |
| ]) |
| ]), |
| ]); |
| }); |
| |
| test( |
| 'unless the tested type is not a subtype of the previously ' |
| 'promoted type', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| if_(x.is_('String'), [ |
| if_(x.is_('int'), [ |
| checkPromoted(x, 'String'), |
| ], [ |
| checkPromoted(x, 'String'), |
| ]) |
| ]), |
| ]); |
| }); |
| |
| test('even when the declared type is a type variable', () { |
| h.enableLegacy(); |
| h.addPromotionException('T', 'int', 'T&int'); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'T'), |
| if_(x.is_('int'), [ |
| checkPromoted(x, 'T&int'), |
| ]), |
| ]); |
| }); |
| }); |
| |
| test('handles arbitrary expressions', () { |
| h.enableLegacy(); |
| h.run([ |
| if_(expr('Object').is_('int'), []), |
| ]); |
| }); |
| }); |
| |
| test('forwardExpression does not re-activate a deeply nested expression', |
| () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| if_(x.is_('int').eq(expr('Object')).thenStmt(block([])), [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| |
| test( |
| 'parenthesizedExpression does not re-activate a deeply nested ' |
| 'expression', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| if_(x.is_('int').eq(expr('Object')).parenthesized, [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| |
| test('variableRead returns the promoted type if promoted', () { |
| h.enableLegacy(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| if_( |
| x |
| .readAndCheckPromotedType((type) => expect(type, isNull)) |
| .is_('int'), |
| [ |
| x.readAndCheckPromotedType((type) => expect(type!.type, 'int')), |
| ]), |
| ]); |
| }); |
| }); |
| |
| group('why not promoted', () { |
| test('due to assignment', () { |
| var x = Var('x'); |
| late Expression writeExpression; |
| h.run([ |
| declare(x, type: 'int?', initializer: expr('int?')), |
| if_(x.eq(nullLiteral), [ |
| return_(), |
| ]), |
| checkPromoted(x, 'int'), |
| (writeExpression = x.write(expr('int?'))), |
| checkNotPromoted(x), |
| x.whyNotPromoted((reasons) { |
| expect(reasons.keys, unorderedEquals([Type('int')])); |
| var nonPromotionReason = |
| reasons.values.single as DemoteViaExplicitWrite<Var>; |
| expect(nonPromotionReason.node, same(writeExpression)); |
| expect(nonPromotionReason.documentationLink, |
| NonPromotionDocumentationLink.write); |
| }), |
| ]); |
| }); |
| |
| test('due to assignment, multiple demotions', () { |
| var x = Var('x'); |
| late Expression writeExpression; |
| h.run([ |
| declare(x, type: 'Object?', initializer: expr('Object?')), |
| if_(x.isNot('int?'), [ |
| return_(), |
| ]), |
| if_(x.eq(nullLiteral), [ |
| return_(), |
| ]), |
| checkPromoted(x, 'int'), |
| (writeExpression = x.write(expr('Object?'))), |
| checkNotPromoted(x), |
| x.whyNotPromoted((reasons) { |
| expect(reasons.keys, unorderedEquals([Type('int'), Type('int?')])); |
| for (var type in [ |
| SharedTypeView(Type('int')), |
| SharedTypeView(Type('int?')) |
| ]) { |
| var nonPromotionReason = |
| reasons[type] as DemoteViaExplicitWrite<Var>; |
| expect(nonPromotionReason.node, same(writeExpression)); |
| expect(nonPromotionReason.documentationLink, |
| NonPromotionDocumentationLink.write); |
| } |
| }), |
| ]); |
| }); |
| |
| test('due to pattern assignment', () { |
| var x = Var('x'); |
| late Pattern writePattern; |
| h.run([ |
| declare(x, type: 'int?', initializer: expr('int?')), |
| if_(x.eq(nullLiteral), [ |
| return_(), |
| ]), |
| checkPromoted(x, 'int'), |
| (writePattern = x.pattern()).assign(expr('int?')), |
| checkNotPromoted(x), |
| x.whyNotPromoted((reasons) { |
| expect(reasons.keys, unorderedEquals([Type('int')])); |
| var nonPromotionReason = |
| reasons.values.single as DemoteViaExplicitWrite<Var>; |
| expect(nonPromotionReason.node, same(writePattern)); |
| expect(nonPromotionReason.documentationLink, |
| NonPromotionDocumentationLink.write); |
| }), |
| ]); |
| }); |
| |
| test('preserved in join when one branch unreachable', () { |
| var x = Var('x'); |
| late Expression writeExpression; |
| h.run([ |
| declare(x, type: 'int?', initializer: expr('int?')), |
| if_(x.eq(nullLiteral), [ |
| return_(), |
| ]), |
| checkPromoted(x, 'int'), |
| (writeExpression = x.write(expr('int?'))), |
| checkNotPromoted(x), |
| if_(expr('bool'), [ |
| return_(), |
| ]), |
| x.whyNotPromoted((reasons) { |
| expect(reasons.keys, unorderedEquals([Type('int')])); |
| var nonPromotionReason = |
| reasons.values.single as DemoteViaExplicitWrite<Var>; |
| expect(nonPromotionReason.node, same(writeExpression)); |
| expect(nonPromotionReason.documentationLink, |
| NonPromotionDocumentationLink.write); |
| }), |
| ]); |
| }); |
| |
| test('preserved in later promotions', () { |
| var x = Var('x'); |
| late Expression writeExpression; |
| h.run([ |
| declare(x, type: 'Object', initializer: expr('Object')), |
| if_(x.is_('int', isInverted: true), [ |
| return_(), |
| ]), |
| checkPromoted(x, 'int'), |
| (writeExpression = x.write(expr('Object'))), |
| checkNotPromoted(x), |
| if_(x.is_('num', isInverted: true), [ |
| return_(), |
| ]), |
| checkPromoted(x, 'num'), |
| x.whyNotPromoted((reasons) { |
| var nonPromotionReason = reasons[SharedTypeView(Type('int'))] |
| as DemoteViaExplicitWrite<Var>; |
| expect(nonPromotionReason.node, same(writeExpression)); |
| expect(nonPromotionReason.documentationLink, |
| NonPromotionDocumentationLink.write); |
| }), |
| ]); |
| }); |
| |
| test('re-promotion', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'int?', initializer: expr('int?')), |
| if_(x.eq(nullLiteral), [ |
| return_(), |
| ]), |
| checkPromoted(x, 'int'), |
| x.write(expr('int?')), |
| checkNotPromoted(x), |
| if_(x.eq(nullLiteral), [ |
| return_(), |
| ]), |
| checkPromoted(x, 'int'), |
| x.whyNotPromoted((reasons) { |
| expect(reasons, isEmpty); |
| }), |
| ]); |
| }); |
| |
| group('field promotion disabled', () { |
| test('via explicit this', () { |
| h.disableFieldPromotion(); |
| h.thisType = 'C'; |
| h.addMember('C', '_field', 'Object?', promotable: true); |
| h.run([ |
| if_(this_.property('_field').eq(nullLiteral), [ |
| return_(), |
| ]), |
| this_.property('_field').whyNotPromoted((reasons) { |
| expect(reasons.keys, unorderedEquals([Type('Object')])); |
| var nonPromotionReason = reasons.values.single |
| as PropertyNotPromotedForNonInherentReason; |
| expect(nonPromotionReason.fieldPromotionEnabled, false); |
| }), |
| ]); |
| }); |
| |
| test('via implicit this/super', () { |
| h.disableFieldPromotion(); |
| h.thisType = 'C'; |
| h.addMember('C', '_field', 'Object?', promotable: true); |
| h.run([ |
| if_(thisProperty('_field').eq(nullLiteral), [ |
| return_(), |
| ]), |
| thisProperty('_field').whyNotPromoted((reasons) { |
| expect(reasons.keys, unorderedEquals([Type('Object')])); |
| var nonPromotionReason = reasons.values.single |
| as PropertyNotPromotedForNonInherentReason; |
| expect(nonPromotionReason.fieldPromotionEnabled, false); |
| }), |
| ]); |
| }); |
| |
| test('via variable', () { |
| h.disableFieldPromotion(); |
| h.addMember('C', '_field', 'Object?', promotable: true); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'C', initializer: expr('C')), |
| if_(x.property('_field').eq(nullLiteral), [ |
| return_(), |
| ]), |
| x.property('_field').whyNotPromoted((reasons) { |
| expect(reasons.keys, unorderedEquals([Type('Object')])); |
| var nonPromotionReason = reasons.values.single |
| as PropertyNotPromotedForNonInherentReason; |
| expect(nonPromotionReason.fieldPromotionEnabled, false); |
| }), |
| ]); |
| }); |
| }); |
| |
| group('because this', () { |
| test('explicit', () { |
| h.thisType = 'C'; |
| h.addSuperInterfaces('D', (_) => [Type('C'), Type('Object')]); |
| h.addSuperInterfaces('C', (_) => [Type('Object')]); |
| h.run([ |
| if_(this_.isNot('D'), [ |
| return_(), |
| ]), |
| this_.whyNotPromoted((reasons) { |
| expect(reasons.keys, unorderedEquals([Type('D')])); |
| var nonPromotionReason = reasons.values.single as ThisNotPromoted; |
| expect(nonPromotionReason.documentationLink, |
| NonPromotionDocumentationLink.this_); |
| }), |
| ]); |
| }); |
| |
| test('implicit', () { |
| h.thisType = 'C'; |
| h.addSuperInterfaces('D', (_) => [Type('C'), Type('Object')]); |
| h.addSuperInterfaces('C', (_) => [Type('Object')]); |
| h.run([ |
| if_(this_.isNot('D'), [ |
| return_(), |
| ]), |
| implicitThis_whyNotPromoted('C', (reasons) { |
| expect(reasons.keys, unorderedEquals([Type('D')])); |
| var nonPromotionReason = reasons.values.single as ThisNotPromoted; |
| expect(nonPromotionReason.documentationLink, |
| NonPromotionDocumentationLink.this_); |
| }), |
| ]); |
| }); |
| }); |
| }); |
| |
| group('Field promotion', () { |
| test('promotable field', () { |
| h.addMember('C', '_field', 'Object?', promotable: true); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'C', initializer: expr('C')), |
| if_(x.property('_field').eq(nullLiteral), [ |
| return_(), |
| ]), |
| checkPromoted(x.property('_field'), 'Object'), |
| x.property('_field').checkType('Object'), |
| ]); |
| }); |
| |
| test('promotable field, this', () { |
| h.thisType = 'C'; |
| h.addMember('C', '_field', 'Object?', promotable: true); |
| h.run([ |
| if_(thisProperty('_field').eq(nullLiteral), [ |
| return_(), |
| ]), |
| checkPromoted(thisProperty('_field'), 'Object'), |
| thisProperty('_field').checkType('Object'), |
| ]); |
| }); |
| |
| test('non-promotable field', () { |
| h.addMember('C', '_field', 'Object?', promotable: false); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'C', initializer: expr('C')), |
| if_(x.property('_field').eq(nullLiteral), [ |
| return_(), |
| ]), |
| checkNotPromoted(x.property('_field')), |
| x.property('_field').checkType('Object?'), |
| ]); |
| }); |
| |
| test('non-promotable field, this', () { |
| h.thisType = 'C'; |
| h.addMember('C', '_field', 'Object?', promotable: false); |
| h.run([ |
| if_(thisProperty('_field').eq(nullLiteral), [ |
| return_(), |
| ]), |
| checkNotPromoted(thisProperty('_field')), |
| thisProperty('_field').checkType('Object?'), |
| ]); |
| }); |
| |
| test('multiply promoted', () { |
| h.addMember('C', '_field', 'Object?', promotable: true); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'C', initializer: expr('C')), |
| if_(x.property('_field').eq(nullLiteral), [ |
| return_(), |
| ]), |
| if_(x.property('_field').isNot('int'), [ |
| return_(), |
| ]), |
| checkPromoted(x.property('_field'), 'int'), |
| x.property('_field').checkType('int'), |
| ]); |
| }); |
| |
| test('multiply promoted, this', () { |
| h.thisType = 'C'; |
| h.addMember('C', '_field', 'Object?', promotable: true); |
| h.run([ |
| if_(thisProperty('_field').eq(nullLiteral), [ |
| return_(), |
| ]), |
| if_(thisProperty('_field').isNot('int'), [ |
| return_(), |
| ]), |
| checkPromoted(thisProperty('_field'), 'int'), |
| thisProperty('_field').checkType('int'), |
| ]); |
| }); |
| |
| test('promotion of target breaks field promotion', () { |
| h.addMember('B', '_field', 'Object?', promotable: true); |
| h.addMember('C', '_field', 'num?', promotable: true); |
| h.addSuperInterfaces('C', (_) => [Type('B'), Type('Object')]); |
| h.addSuperInterfaces('B', (_) => [Type('Object')]); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'B', initializer: expr('B')), |
| if_(x.property('_field').eq(nullLiteral), [ |
| return_(), |
| ]), |
| checkPromoted(x.property('_field'), 'Object'), |
| x.property('_field').checkType('Object'), |
| if_(x.isNot('C'), [ |
| return_(), |
| ]), |
| checkNotPromoted(x.property('_field')), |
| x.property('_field').checkType('num?'), |
| ]); |
| }); |
| |
| test('promotion of target does not break field promotion', () { |
| h.addMember('B', '_field', 'Object?', promotable: true); |
| h.addMember('C', '_field', 'num?', promotable: true); |
| h.addSuperInterfaces('C', (_) => [Type('B'), Type('Object')]); |
| h.addSuperInterfaces('B', (_) => [Type('Object')]); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'B', initializer: expr('B')), |
| if_(x.property('_field').isNot('int'), [ |
| return_(), |
| ]), |
| checkPromoted(x.property('_field'), 'int'), |
| x.property('_field').checkType('int'), |
| if_(x.isNot('C'), [ |
| return_(), |
| ]), |
| checkPromoted(x.property('_field'), 'int'), |
| x.property('_field').checkType('int'), |
| ]); |
| }); |
| |
| test('field not promotable after outer variable demoted', () { |
| h.addMember('B', '_field', 'Object?', promotable: false); |
| h.addMember('C', '_field', 'Object?', promotable: true); |
| h.addSuperInterfaces('C', (_) => [Type('B'), Type('Object')]); |
| h.addSuperInterfaces('B', (_) => [Type('Object')]); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'B', initializer: expr('B')), |
| if_(x.is_('C'), [ |
| if_(x.property('_field').notEq(nullLiteral), [ |
| checkPromoted(x.property('_field'), 'Object'), |
| x.property('_field').checkType('Object'), |
| ]), |
| ]), |
| if_(x.property('_field').notEq(nullLiteral), [ |
| checkNotPromoted(x.property('_field')), |
| x.property('_field').checkType('Object?'), |
| ]), |
| ]); |
| }); |
| |
| test('field promotable after outer variable promoted', () { |
| h.addMember('B', '_field', 'Object?', promotable: false); |
| h.addMember('C', '_field', 'Object?', promotable: true); |
| h.addSuperInterfaces('C', (_) => [Type('B'), Type('Object')]); |
| h.addSuperInterfaces('B', (_) => [Type('Object')]); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'B', initializer: expr('B')), |
| if_(x.property('_field').notEq(nullLiteral), [ |
| checkNotPromoted(x.property('_field')), |
| x.property('_field').checkType('Object?'), |
| ]), |
| if_(x.is_('C'), [ |
| if_(x.property('_field').notEq(nullLiteral), [ |
| checkPromoted(x.property('_field'), 'Object'), |
| x.property('_field').checkType('Object'), |
| ]), |
| ]), |
| ]); |
| }); |
| |
| test('promotion targets properly distinguished', () { |
| h.thisType = 'C'; |
| h.addMember('C', '_field1', 'Object?', promotable: true); |
| h.addMember('C', '_field2', 'Object?', promotable: true); |
| var x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(x, type: 'C', initializer: expr('C')), |
| declare(y, type: 'C', initializer: expr('C')), |
| if_(thisProperty('_field1').isNot('String'), [ |
| return_(), |
| ]), |
| if_(this_.property('_field2').isNot('String?'), [ |
| return_(), |
| ]), |
| if_(x.property('_field1').isNot('int'), [ |
| return_(), |
| ]), |
| if_(y.property('_field1').isNot('double'), [ |
| return_(), |
| ]), |
| checkPromoted(thisProperty('_field1'), 'String'), |
| thisProperty('_field1').checkType('String'), |
| checkPromoted(this_.property('_field1'), 'String'), |
| this_.property('_field1').checkType('String'), |
| checkPromoted(thisProperty('_field2'), 'String?'), |
| thisProperty('_field2').checkType('String?'), |
| checkPromoted(this_.property('_field2'), 'String?'), |
| this_.property('_field2').checkType('String?'), |
| checkPromoted(x.property('_field1'), 'int'), |
| x.property('_field1').checkType('int'), |
| checkNotPromoted(x.property('_field2')), |
| x.property('_field2').checkType('Object?'), |
| checkPromoted(y.property('_field1'), 'double'), |
| y.property('_field1').checkType('double'), |
| checkNotPromoted(y.property('_field2')), |
| y.property('_field2').checkType('Object?'), |
| ]); |
| }); |
| |
| test('cancelled by write to local var', () { |
| h.thisType = 'C'; |
| h.addMember('C', '_field', 'Object?', promotable: true); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'C', initializer: expr('C')), |
| if_(x.property('_field').isNot('String'), [ |
| return_(), |
| ]), |
| checkPromoted(x.property('_field'), 'String'), |
| x.property('_field').checkType('String'), |
| x.write(expr('C')), |
| checkNotPromoted(x.property('_field')), |
| x.property('_field').checkType('Object?'), |
| ]); |
| }); |
| |
| test('cancelled by write to local var, nested', () { |
| h.thisType = 'C'; |
| h.addMember('C', '_field1', 'D', promotable: true); |
| h.addMember('D', '_field2', 'Object?', promotable: true); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'C', initializer: expr('C')), |
| if_(x.property('_field1').property('_field2').isNot('String'), [ |
| return_(), |
| ]), |
| checkPromoted(x.property('_field1').property('_field2'), 'String'), |
| x.property('_field1').property('_field2').checkType('String'), |
| x.write(expr('C')), |
| checkNotPromoted(x.property('_field1').property('_field2')), |
| x.property('_field1').property('_field2').checkType('Object?'), |
| ]); |
| }); |
| |
| test('cancelled by write to local var later in loop', () { |
| h.thisType = 'C'; |
| h.addMember('C', '_field', 'Object?', promotable: true); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'C', initializer: expr('C')), |
| if_(x.property('_field').isNot('String'), [ |
| return_(), |
| ]), |
| checkPromoted(x.property('_field'), 'String'), |
| x.property('_field').checkType('String'), |
| while_(expr('bool'), [ |
| checkNotPromoted(x.property('_field')), |
| x.property('_field').checkType('Object?'), |
| x.write(expr('C')), |
| ]), |
| ]); |
| }); |
| |
| test('cancelled by write to local var later in loop, nested', () { |
| h.thisType = 'C'; |
| h.addMember('C', '_field1', 'D', promotable: true); |
| h.addMember('D', '_field2', 'Object?', promotable: true); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'C', initializer: expr('C')), |
| if_(x.property('_field1').property('_field2').isNot('String'), [ |
| return_(), |
| ]), |
| checkPromoted(x.property('_field1').property('_field2'), 'String'), |
| x.property('_field1').property('_field2').checkType('String'), |
| while_(expr('bool'), [ |
| checkNotPromoted(x.property('_field1').property('_field2')), |
| x.property('_field1').property('_field2').checkType('Object?'), |
| x.write(expr('C')), |
| ]), |
| ]); |
| }); |
| |
| test('cancelled by capture of local var', () { |
| h.thisType = 'C'; |
| h.addMember('C', '_field', 'Object?', promotable: true); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'C', initializer: expr('C')), |
| if_(x.property('_field').isNot('String'), [ |
| return_(), |
| ]), |
| checkPromoted(x.property('_field'), 'String'), |
| x.property('_field').checkType('String'), |
| localFunction([ |
| x.write(expr('C')), |
| ]), |
| checkNotPromoted(x.property('_field')), |
| x.property('_field').checkType('Object?'), |
| ]); |
| }); |
| |
| test('cancelled by capture of local var, nested', () { |
| h.thisType = 'C'; |
| h.addMember('C', '_field1', 'D', promotable: true); |
| h.addMember('D', '_field2', 'Object?', promotable: true); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'C', initializer: expr('C')), |
| if_(x.property('_field1').property('_field2').isNot('String'), [ |
| return_(), |
| ]), |
| checkPromoted(x.property('_field1').property('_field2'), 'String'), |
| x.property('_field1').property('_field2').checkType('String'), |
| localFunction([ |
| x.write(expr('C')), |
| ]), |
| checkNotPromoted(x.property('_field1').property('_field2')), |
| x.property('_field1').property('_field2').checkType('Object?'), |
| ]); |
| }); |
| |
| test('prevented by previous capture of local var', () { |
| h.thisType = 'C'; |
| h.addMember('C', '_field', 'Object?', promotable: true); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'C', initializer: expr('C')), |
| localFunction([ |
| x.write(expr('C')), |
| ]), |
| if_(x.property('_field').isNot('String'), [ |
| return_(), |
| ]), |
| checkNotPromoted(x.property('_field')), |
| x.property('_field').checkType('Object?'), |
| ]); |
| }); |
| |
| test('prevented by previous capture of local var, nested', () { |
| h.thisType = 'C'; |
| h.addMember('C', '_field1', 'D', promotable: true); |
| h.addMember('D', '_field2', 'Object?', promotable: true); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'C', initializer: expr('C')), |
| localFunction([ |
| x.write(expr('C')), |
| ]), |
| if_(x.property('_field1').property('_field2').isNot('String'), [ |
| return_(), |
| ]), |
| checkNotPromoted(x.property('_field1').property('_field2')), |
| x.property('_field1').property('_field2').checkType('Object?'), |
| ]); |
| }); |
| |
| test('prevented by non-promotability of target', () { |
| h.thisType = 'C'; |
| h.addMember('C', '_field1', 'D', promotable: false); |
| h.addMember('D', '_field2', 'Object?', promotable: true); |
| h.run([ |
| if_(thisProperty('_field1').property('_field2').isNot('String'), [ |
| return_(), |
| ]), |
| checkNotPromoted(thisProperty('_field1').property('_field2')), |
| thisProperty('_field1').property('_field2').checkType('Object?'), |
| ]); |
| }); |
| |
| test('super tracked separately', () { |
| // This test verifies that promotion of `this._field` and promotion of |
| // `super._field` are tracked separately. This is necessary in case |
| // `this._field` overrides `super._field` (and hence the two accesses |
| // refer to different underlying fields). |
| h.thisType = 'C'; |
| h.addMember('C', '_field', 'int?', promotable: true); |
| h.run([ |
| if_(thisProperty('_field').notEq(nullLiteral), [ |
| checkPromoted(thisProperty('_field'), 'int'), |
| this_.property('_field').checkType('int'), |
| checkNotPromoted(superProperty('_field')), |
| ]), |
| if_(superProperty('_field').notEq(nullLiteral), [ |
| checkPromoted(superProperty('_field'), 'int'), |
| checkNotPromoted(thisProperty('_field')), |
| this_.property('_field').checkType('int?'), |
| ]), |
| ]); |
| }); |
| |
| group('cascades:', () { |
| group('not null-aware:', () { |
| test('cascaded access receives the benefit of promotion', () { |
| h.addMember('C', '_field', 'Object?', promotable: true); |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('C')), |
| x.property('_field').as_('int'), |
| checkPromoted(x.property('_field'), 'int'), |
| x.cascade([ |
| (v) => v.property('_field').checkType('int'), |
| (v) => v.property('_field').checkType('int'), |
| ]), |
| ]); |
| }); |
| |
| test('field access on cascade expression retains promotion', () { |
| h.addMember('C', '_field', 'Object?', promotable: true); |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('C')), |
| x.property('_field').as_('int'), |
| checkPromoted(x.property('_field'), 'int'), |
| x |
| .cascade([(v) => v.property('_field').checkType('int')]) |
| .property('_field') |
| .checkType('int'), |
| ]); |
| }); |
| |
| test('a cascade expression is not promotable', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('int?')), |
| x.cascade([(v) => v.invokeMethod('toString', [])]).nonNullAssert, |
| checkNotPromoted(x), |
| ]); |
| }); |
| |
| test('even a field of an ephemeral object can be promoted', () { |
| h.addMember('C', '_field', 'int?', promotable: true); |
| h.run([ |
| expr('C') |
| .cascade([ |
| (v) => v.property('_field').checkType('int?').nonNullAssert, |
| (v) => v.property('_field').checkType('int'), |
| ]) |
| .property('_field') |
| .checkType('int'), |
| ]); |
| }); |
| |
| test('even a field of a write captured variable can be promoted', () { |
| h.addMember('C', '_field', 'int?', promotable: true); |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('C')), |
| localFunction([ |
| x.write(expr('C')), |
| ]), |
| x |
| .cascade([ |
| (v) => v.property('_field').checkType('int?').nonNullAssert, |
| (v) => v.property('_field').checkType('int'), |
| ]) |
| .property('_field') |
| .checkType('int'), |
| ]); |
| }); |
| }); |
| |
| group('null-aware:', () { |
| test('cascaded access receives the benefit of promotion', () { |
| h.addMember('C', '_field', 'Object?', promotable: true); |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('C')), |
| x.property('_field').as_('int'), |
| checkPromoted(x.property('_field'), 'int'), |
| x.cascade(isNullAware: true, [ |
| (v) => v.property('_field').checkType('int'), |
| (v) => v.property('_field').checkType('int'), |
| ]), |
| ]); |
| }); |
| |
| test('field access on cascade expression retains promotion', () { |
| h.addMember('C', '_field', 'Object?', promotable: true); |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('C')), |
| x.property('_field').as_('int'), |
| checkPromoted(x.property('_field'), 'int'), |
| x |
| .cascade( |
| isNullAware: true, |
| [(v) => v.property('_field').checkType('int')]) |
| .property('_field') |
| .checkType('int'), |
| ]); |
| }); |
| |
| test('a cascade expression is not promotable', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('int?')), |
| x.cascade( |
| isNullAware: true, |
| [(v) => v.invokeMethod('toString', [])]).nonNullAssert, |
| checkNotPromoted(x), |
| ]); |
| }); |
| |
| test('even a field of an ephemeral object can be promoted', () { |
| h.addMember('C', '_field', 'int?', promotable: true); |
| h.addSuperInterfaces('C', (_) => [Type('Object')]); |
| h.run([ |
| expr('C?') |
| .cascade(isNullAware: true, [ |
| (v) => v.property('_field').checkType('int?').nonNullAssert, |
| (v) => v.property('_field').checkType('int'), |
| ]) |
| // But the promotion doesn't survive beyond the cascade |
| // expression, because of the implicit control flow join implied |
| // by the null-awareness of the cascade. (In principle it would |
| // be sound to preserve the promotion, but it's extra work to do |
| // so, and it's not clear that there would be enough user |
| // benefit to justify the work). |
| .nonNullAssert |
| .property('_field') |
| .checkType('int?'), |
| ]); |
| }); |
| |
| test('even a field of a write captured variable can be promoted', () { |
| h.addMember('C', '_field', 'int?', promotable: true); |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('C')), |
| localFunction([ |
| x.write(expr('C')), |
| ]), |
| x |
| .cascade(isNullAware: true, [ |
| (v) => v.property('_field').checkType('int?').nonNullAssert, |
| (v) => v.property('_field').checkType('int'), |
| ]) |
| // But the promotion doesn't survive beyond the cascade |
| // expression, because of the implicit control flow join implied |
| // by the null-awareness of the cascade. (In principle it would |
| // be sound to preserve the promotion, but it's extra work to do |
| // so, and it's not clear that there would be enough user |
| // benefit to justify the work). |
| .property('_field') |
| .checkType('int?'), |
| ]); |
| }); |
| }); |
| |
| test('unstable target', () { |
| h.addMember('C', 'd', 'D', promotable: false); |
| h.addMember('D', '_i', 'int?', promotable: true); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| // The value of `c.d` is cached in a temporary variable, call it `t0`. |
| c.property('d').cascade([ |
| // `t0._i` could be null at this point. |
| (t0) => t0.property('_i').checkType('int?'), |
| // But now we promote it to non-null |
| (t0) => t0.property('_i').nonNullAssert, |
| // And the promotion sticks for the duration of the cascade. |
| (t0) => t0.property('_i').checkType('int'), |
| ]), |
| // Now, a new value of `c.d` is computed, and cached in a new |
| // temporary variable, call it `t1`. |
| c.property('d').cascade([ |
| // even though `t0._i` was promoted above, `t1._i` could still be |
| // null at this point. |
| (t1) => t1.property('_i').checkType('int?'), |
| // But now we promote it to non-null |
| (t1) => t1.property('_i').nonNullAssert, |
| // And the promotion sticks for the duration of the cascade. |
| (t1) => t1.property('_i').checkType('int'), |
| ]), |
| ]); |
| }); |
| }); |
| |
| test('field becomes promotable after type test', () { |
| // In this test, `C._property` is not promotable, but `D` extends `C`, and |
| // `D._property` is promotable. (This could happen if, for example, |
| // `C._property` is an abstract getter, and `D._property` is a final |
| // field). If `_property` is type-tested while the type of the target is |
| // `C`, but then `_property` is accessed while the type of the target is |
| // `D`, no promotion occurs, because the thing that is type tested is |
| // non-promotable. |
| h.addMember('C', '_property', 'int?', promotable: false); |
| h.addMember('D', '_property', 'int?', promotable: true); |
| h.addSuperInterfaces('D', (_) => [Type('C'), Type('Object')]); |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('C')), |
| x.property('_property').nonNullAssert, |
| x.as_('D'), |
| checkNotPromoted(x.property('_property')), |
| x.property('_property').nonNullAssert, |
| checkPromoted(x.property('_property'), 'int'), |
| ]); |
| }); |
| |
| group('Preserved by join:', () { |
| test('Property', () { |
| h.addMember('C', '_field', 'int?', promotable: true); |
| var x = Var('x'); |
| // Even though the two branches of the "if" assign different values to |
| // `x` (and hence the SSA nodes associated with `x._field` in the two |
| // branches are different), the promotion is still preserved by the |
| // join. |
| h.run([ |
| declare(x, type: 'C'), |
| if_(expr('bool'), [ |
| x.write(expr('C')), |
| x.property('_field').nonNullAssert, |
| ], [ |
| x.write(expr('C')), |
| x.property('_field').nonNullAssert, |
| ]), |
| checkPromoted(x.property('_field'), 'int'), |
| ]); |
| }); |
| |
| test('Property of property', () { |
| h.addMember('C', '_i', 'int?', promotable: true); |
| h.addMember('D', '_c', 'C', promotable: true); |
| var x = Var('x'); |
| // Even though the two branches of the "if" assign different values to |
| // `x` (and hence the SSA nodes associated with `x._c._i` in the two |
| // branches are different), the promotion is still preserved by the |
| // join. |
| h.run([ |
| declare(x, type: 'D'), |
| if_(expr('bool'), [ |
| x.write(expr('D')), |
| x.property('_c').property('_i').nonNullAssert, |
| ], [ |
| x.write(expr('D')), |
| x.property('_c').property('_i').nonNullAssert, |
| ]), |
| checkPromoted(x.property('_c').property('_i'), 'int'), |
| ]); |
| }); |
| |
| test('Property promoted only in first joined control flow path', () { |
| h.addMember('C', '_field', 'int?', promotable: true); |
| var x = Var('x'); |
| // No promotion because the property is only promoted in one control |
| // flow path. |
| h.run([ |
| declare(x, type: 'C'), |
| if_(expr('bool'), [ |
| x.write(expr('C')), |
| x.property('_field').nonNullAssert, |
| ], [ |
| x.write(expr('C')), |
| x.property('_field'), |
| ]), |
| checkNotPromoted(x.property('_field')), |
| ]); |
| }); |
| |
| test('Property promoted only in second joined control flow path', () { |
| h.addMember('C', '_field', 'int?', promotable: true); |
| var x = Var('x'); |
| // No promotion because the property is only promoted in one control |
| // flow path. |
| h.run([ |
| declare(x, type: 'C'), |
| if_(expr('bool'), [ |
| x.write(expr('C')), |
| x.property('_field'), |
| ], [ |
| x.write(expr('C')), |
| x.property('_field').nonNullAssert, |
| ]), |
| checkNotPromoted(x.property('_field')), |
| ]); |
| }); |
| |
| test('Property accessed only in first joined control flow path', () { |
| h.addMember('C', '_field', 'int?', promotable: true); |
| var x = Var('x'); |
| // No promotion because the property is only promoted in one control |
| // flow path. |
| h.run([ |
| declare(x, type: 'C'), |
| if_(expr('bool'), [ |
| x.write(expr('C')), |
| x.property('_field').nonNullAssert, |
| ], [ |
| x.write(expr('C')), |
| ]), |
| checkNotPromoted(x.property('_field')), |
| ]); |
| }); |
| |
| test('Property accessed only in second joined control flow path', () { |
| h.addMember('C', '_field', 'int?', promotable: true); |
| var x = Var('x'); |
| // No promotion because the property is only promoted in one control |
| // flow path. |
| h.run([ |
| declare(x, type: 'C'), |
| if_(expr('bool'), [ |
| x.write(expr('C')), |
| ], [ |
| x.write(expr('C')), |
| x.property('_field').nonNullAssert, |
| ]), |
| checkNotPromoted(x.property('_field')), |
| ]); |
| }); |
| }); |
| |
| group('In try/finally:', () { |
| // In a try/finally statement, the `finally` clause is analyzed as though |
| // the `try` block hasn't executed yet (and any variables written inside |
| // the `try` block have been de-promoted), to account for the fact that |
| // an exception might occur at any time during the `try` block. However, |
| // after the `finally` block is finished, any flow model changes that |
| // occurred during the `finally` block are rewound and re-applied to the |
| // flow model state after the `try` block, to account for the fact that |
| // if the try/finally statement completes normally, it is known that the |
| // `try` block executed fully. |
| // |
| // We need to verify that this rebasing logic handles all the possible |
| // ways that field promotion can occur relative to a try/finally |
| // statement. |
| |
| test('Promoted in try', () { |
| h.addMember('C', '_property', 'int?', promotable: true); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| try_([ |
| checkNotPromoted(c.property('_property')), |
| c.property('_property').nonNullAssert, |
| checkPromoted(c.property('_property'), 'int'), |
| ]).finally_([ |
| checkNotPromoted(c.property('_property')), |
| ]), |
| checkPromoted(c.property('_property'), 'int'), |
| ]); |
| }); |
| |
| test('Promoted in try, nested', () { |
| h.addMember('C', '_i', 'int?', promotable: true); |
| h.addMember('D', '_c', 'C', promotable: true); |
| var d = Var('d'); |
| h.run([ |
| declare(d, initializer: expr('D')), |
| try_([ |
| checkNotPromoted(d.property('_c').property('_i')), |
| d.property('_c').property('_i').nonNullAssert, |
| checkPromoted(d.property('_c').property('_i'), 'int'), |
| ]).finally_([ |
| checkNotPromoted(d.property('_c').property('_i')), |
| ]), |
| checkPromoted(d.property('_c').property('_i'), 'int'), |
| ]); |
| }); |
| |
| test('Promoted before try/finally', () { |
| h.addMember('C', '_property', 'int?', promotable: true); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| c.property('_property').nonNullAssert, |
| checkPromoted(c.property('_property'), 'int'), |
| try_([ |
| checkPromoted(c.property('_property'), 'int'), |
| ]).finally_([ |
| checkPromoted(c.property('_property'), 'int'), |
| ]), |
| checkPromoted(c.property('_property'), 'int'), |
| ]); |
| }); |
| |
| test('Promoted before try/finally and in try', () { |
| h.addMember('C', '_property', 'num?', promotable: true); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| c.property('_property').nonNullAssert, |
| checkPromoted(c.property('_property'), 'num'), |
| try_([ |
| checkPromoted(c.property('_property'), 'num'), |
| c.property('_property').as_('int'), |
| checkPromoted(c.property('_property'), 'int'), |
| ]).finally_([ |
| checkPromoted(c.property('_property'), 'num'), |
| ]), |
| checkPromoted(c.property('_property'), 'int'), |
| ]); |
| }); |
| |
| group('Promoted in both try and finally:', () { |
| test('same type', () { |
| h.addMember('C', '_property', 'int?', promotable: true); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| try_([ |
| checkNotPromoted(c.property('_property')), |
| c.property('_property').nonNullAssert, |
| checkPromoted(c.property('_property'), 'int'), |
| ]).finally_([ |
| checkNotPromoted(c.property('_property')), |
| c.property('_property').nonNullAssert, |
| checkPromoted(c.property('_property'), 'int'), |
| ]), |
| checkPromoted(c.property('_property'), 'int'), |
| ]); |
| }); |
| |
| test('finally type is subtype of try type', () { |
| h.addMember('C', '_property', 'num?', promotable: true); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| try_([ |
| checkNotPromoted(c.property('_property')), |
| c.property('_property').nonNullAssert, |
| checkPromoted(c.property('_property'), 'num'), |
| ]).finally_([ |
| checkNotPromoted(c.property('_property')), |
| c.property('_property').as_('int'), |
| checkPromoted(c.property('_property'), 'int'), |
| ]), |
| checkPromoted(c.property('_property'), 'int'), |
| ]); |
| }); |
| |
| test('finally type is supertype of try type', () { |
| h.addMember('C', '_property', 'num?', promotable: true); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| try_([ |
| checkNotPromoted(c.property('_property')), |
| c.property('_property').as_('int'), |
| checkPromoted(c.property('_property'), 'int'), |
| ]).finally_([ |
| checkNotPromoted(c.property('_property')), |
| c.property('_property').nonNullAssert, |
| checkPromoted(c.property('_property'), 'num'), |
| ]), |
| checkPromoted(c.property('_property'), 'int'), |
| ]); |
| }); |
| }); |
| |
| test('Promoted in finally', () { |
| h.addMember('C', '_property', 'int?', promotable: true); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| try_([ |
| checkNotPromoted(c.property('_property')), |
| ]).finally_([ |
| checkNotPromoted(c.property('_property')), |
| c.property('_property').nonNullAssert, |
| checkPromoted(c.property('_property'), 'int'), |
| ]), |
| checkPromoted(c.property('_property'), 'int'), |
| ]); |
| }); |
| |
| test('Promoted in finally, nested', () { |
| h.addMember('C', '_i', 'int?', promotable: true); |
| h.addMember('D', '_c', 'C', promotable: true); |
| var d = Var('d'); |
| h.run([ |
| declare(d, initializer: expr('D')), |
| try_([ |
| checkNotPromoted(d.property('_c').property('_i')), |
| ]).finally_([ |
| checkNotPromoted(d.property('_c').property('_i')), |
| d.property('_c').property('_i').nonNullAssert, |
| checkPromoted(d.property('_c').property('_i'), 'int'), |
| ]), |
| checkPromoted(d.property('_c').property('_i'), 'int'), |
| ]); |
| }); |
| |
| test('Promoted before try/finally, assigned in try', () { |
| h.addMember('C', '_property', 'int?', promotable: true); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| c.property('_property').nonNullAssert, |
| checkPromoted(c.property('_property'), 'int'), |
| try_([ |
| checkPromoted(c.property('_property'), 'int'), |
| c.write(expr('C')), |
| checkNotPromoted(c.property('_property')), |
| ]).finally_([ |
| checkNotPromoted(c.property('_property')), |
| ]), |
| checkNotPromoted(c.property('_property')), |
| ]); |
| }); |
| |
| test('Promoted before try/finally, assigned and re-promoted in try', () { |
| h.addMember('C', '_property', 'int?', promotable: true); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| c.property('_property').nonNullAssert, |
| checkPromoted(c.property('_property'), 'int'), |
| try_([ |
| checkPromoted(c.property('_property'), 'int'), |
| c.write(expr('C')), |
| c.property('_property').nonNullAssert, |
| checkPromoted(c.property('_property'), 'int'), |
| ]).finally_([ |
| checkNotPromoted(c.property('_property')), |
| ]), |
| checkPromoted(c.property('_property'), 'int'), |
| ]); |
| }); |
| |
| test('Assigned in try, promoted in finally', () { |
| h.addMember('C', '_property', 'int?', promotable: true); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| try_([ |
| // Note: no calls to `checkNotPromoted` here, because we want to |
| // trigger the code path where flow analysis doesn't even know about |
| // the property until the finally block |
| c.write(expr('C')), |
| ]).finally_([ |
| c.property('_property').nonNullAssert, |
| checkPromoted(c.property('_property'), 'int'), |
| ]), |
| checkPromoted(c.property('_property'), 'int'), |
| ]); |
| }); |
| |
| test('Assigned in try, promoted in finally, nested', () { |
| h.addMember('C', '_i', 'int?', promotable: true); |
| h.addMember('D', '_c', 'C', promotable: true); |
| var d = Var('d'); |
| h.run([ |
| declare(d, initializer: expr('D')), |
| try_([ |
| // Note: no calls to `checkNotPromoted` here, because we want to |
| // trigger the code path where flow analysis doesn't even know about |
| // the property until the finally block |
| d.write(expr('D')), |
| ]).finally_([ |
| d.property('_c').property('_i').nonNullAssert, |
| checkPromoted(d.property('_c').property('_i'), 'int'), |
| ]), |
| checkPromoted(d.property('_c').property('_i'), 'int'), |
| ]); |
| }); |
| |
| test('Assigned but not promotable in try, promoted in finally', () { |
| h.addMember('C', '_property', 'int?', promotable: false); |
| h.addMember('D', '_property', 'int?', promotable: true); |
| h.addSuperInterfaces('D', (_) => [Type('C'), Type('Object')]); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| try_([ |
| c.write(expr('C')), |
| c.property('_property').nonNullAssert, |
| checkNotPromoted(c.property('_property')), |
| ]).finally_([ |
| c.as_('D'), |
| c.property('_property').nonNullAssert, |
| checkPromoted(c.property('_property'), 'int'), |
| ]), |
| checkPromoted(c.property('_property'), 'int'), |
| ]); |
| }); |
| |
| test('Assigned and promoted in try, promoted to subtype in finally', () { |
| h.addMember('C', '_property', 'Object', promotable: true); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| try_([ |
| checkNotPromoted(c.property('_property')), |
| c.write(expr('C')), |
| checkNotPromoted(c.property('_property')), |
| c.property('_property').as_('num'), |
| checkPromoted(c.property('_property'), 'num'), |
| ]).finally_([ |
| c.property('_property').as_('int'), |
| checkPromoted(c.property('_property'), 'int'), |
| ]), |
| checkPromoted(c.property('_property'), 'int'), |
| ]); |
| }); |
| }); |
| |
| group('Via local condition variable:', () { |
| group('without intervening promotion:', () { |
| // These tests exercise the code path in `FlowModel.rebaseForward` where |
| // `this` model (which represents the state captured at the time the |
| // condition variable is written) contains a promotion key for the |
| // field, but the `base` model (which represents state just prior to |
| // reading from the condition variable) doesn't contain any promotion |
| // key for the field. Furthermore, since no other promotions occur |
| // between writing and reading the condition variable, `rebaseForward` |
| // will not create a fresh `FlowModel`; it will simply return `this` |
| // model. |
| test('using null check', () { |
| h.addMember('C', '_field', 'int?', promotable: true); |
| var c = Var('c'); |
| var b = Var('b'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| declare(b, initializer: c.property('_field').notEq(nullLiteral)), |
| if_(b, [ |
| checkPromoted(c.property('_field'), 'int'), |
| ]), |
| ]); |
| }); |
| |
| test('using `is` test', () { |
| h.addMember('C', '_field', 'Object', promotable: true); |
| h.addSuperInterfaces('C', (_) => [Type('Object')]); |
| var c = Var('c'); |
| var b = Var('b'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| declare(b, initializer: c.property('_field').is_('int')), |
| if_(b, [ |
| checkPromoted(c.property('_field'), 'int'), |
| ]), |
| ]); |
| }); |
| }); |
| |
| group('with intervening related promotion:', () { |
| // These tests exercise the code path in `FlowModel.rebaseForward` where |
| // `this` model (which represents the state captured at the time the |
| // condition variable is written) and the `base` model (which represents |
| // state just prior to reading from the condition variable) both contain |
| // a promotion key for the field. |
| test('using null check', () { |
| h.addMember('C', '_field', 'int?', promotable: true); |
| var c = Var('c'); |
| var b = Var('b'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| declare(b, initializer: c.property('_field').notEq(nullLiteral)), |
| if_(c.property('_field').notEq(nullLiteral), [ |
| checkPromoted(c.property('_field'), 'int'), |
| ]), |
| if_(b, [ |
| checkPromoted(c.property('_field'), 'int'), |
| ]), |
| ]); |
| }); |
| |
| test('using `is` test', () { |
| h.addMember('C', '_field', 'Object', promotable: true); |
| h.addSuperInterfaces('C', (_) => [Type('Object')]); |
| var c = Var('c'); |
| var b = Var('b'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| declare(b, initializer: c.property('_field').is_('int')), |
| if_(c.property('_field').is_('int'), [ |
| checkPromoted(c.property('_field'), 'int'), |
| ]), |
| if_(b, [ |
| checkPromoted(c.property('_field'), 'int'), |
| ]), |
| ]); |
| }); |
| }); |
| |
| group('with intervening unrelated promotion:', () { |
| // These tests exercise the code path in `FlowModel.rebaseForward` where |
| // `this` model (which represents the state captured at the time the |
| // condition variable is written) contains a promotion key for the |
| // field, but the `base` model (which represents state just prior to |
| // reading from the condition variable) doesn't contain any promotion |
| // key for the field. Since a different variable is promoted in between |
| // writing and reading the condition variable, `rebaseForward` will be |
| // forced to create a fresh `FlowModel`; it will not be able to simply |
| // return `this` model. |
| test('using null check', () { |
| h.addMember('C', '_field', 'int?', promotable: true); |
| var c = Var('c'); |
| var unrelated = Var('unrelated'); |
| var b = Var('b'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| declare(unrelated, initializer: expr('int?')), |
| declare(b, initializer: c.property('_field').notEq(nullLiteral)), |
| unrelated.nonNullAssert, |
| if_(b, [ |
| checkPromoted(c.property('_field'), 'int'), |
| ]), |
| ]); |
| }); |
| |
| test('using `is` test', () { |
| h.addMember('C', '_field', 'Object', promotable: true); |
| h.addSuperInterfaces('C', (_) => [Type('Object')]); |
| var c = Var('c'); |
| var unrelated = Var('unrelated'); |
| var b = Var('b'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| declare(unrelated, initializer: expr('int?')), |
| declare(b, initializer: c.property('_field').is_('int')), |
| unrelated.nonNullAssert, |
| if_(b, [ |
| checkPromoted(c.property('_field'), 'int'), |
| ]), |
| ]); |
| }); |
| }); |
| |
| group('disabled by intervening assignment:', () { |
| test('using null check', () { |
| h.addMember('C', '_field', 'int?', promotable: true); |
| var c = Var('c'); |
| var b = Var('b'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| declare(b, initializer: c.property('_field').notEq(nullLiteral)), |
| if_(c.property('_field').notEq(nullLiteral), [ |
| checkPromoted(c.property('_field'), 'int'), |
| ]), |
| c.write(expr('C')), |
| if_(b, [ |
| checkNotPromoted(c.property('_field')), |
| ]), |
| ]); |
| }); |
| |
| test('using `is` test', () { |
| h.addMember('C', '_field', 'Object', promotable: true); |
| h.addSuperInterfaces('C', (_) => [Type('Object')]); |
| var c = Var('c'); |
| var b = Var('b'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| declare(b, initializer: c.property('_field').is_('int')), |
| if_(c.property('_field').is_('int'), [ |
| checkPromoted(c.property('_field'), 'int'), |
| ]), |
| c.write(expr('C')), |
| if_(b, [ |
| checkNotPromoted(c.property('_field')), |
| ]), |
| ]); |
| }); |
| }); |
| }); |
| |
| group('And object pattern:', () { |
| test('Promotion via object promotion', () { |
| h.addMember('C', '_property', 'int?', promotable: true); |
| h.addDownwardInfer(name: 'C', context: 'C', result: 'C'); |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('C')), |
| ifCase( |
| x, |
| objectPattern( |
| requiredType: 'C', |
| fields: [wildcard().nullCheck.recordField('_property')]), |
| [ |
| checkPromoted(x.property('_property'), 'int'), |
| ], |
| [ |
| checkNotPromoted(x.property('_property')), |
| ]) |
| ]); |
| }); |
| |
| test('Scrutinee restored after object pattern', () { |
| h.addMember('C', '_property', 'int?', promotable: true); |
| h.addDownwardInfer(name: 'C', context: 'C?', result: 'C'); |
| h.addSuperInterfaces('C', (_) => [Type('Object')]); |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('C?')), |
| ifCase( |
| x, |
| objectPattern(requiredType: 'C', fields: [ |
| wildcard().nullCheck.recordField('_property') |
| ]).or( |
| // After visiting the object pattern, the scrutinee should now |
| // be restored to point to the `x`, so this null check should |
| // promote `x` to `C`. |
| wildcard().nullCheck), |
| [ |
| checkPromoted(x, 'C'), |
| ], |
| [ |
| checkNotPromoted(x), |
| ]) |
| ]); |
| }); |
| |
| test('Subpattern matched value type accounts for previous promotion', () { |
| h.addMember('C', '_property', 'int?', promotable: true); |
| h.addDownwardInfer(name: 'C', context: 'C', result: 'C'); |
| var x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(x, initializer: expr('C')), |
| x.property('_property').nonNullAssert, |
| checkPromoted(x.property('_property'), 'int'), |
| ifCase( |
| x, |
| objectPattern(requiredType: 'C', fields: [ |
| y.pattern(expectInferredType: 'int').recordField('_property') |
| ]), |
| []) |
| ]); |
| }); |
| }); |
| |
| group('non promotion reasons:', () { |
| test('inherent reason', () { |
| // It's only necessary to test one of the inherent reasons, because flow |
| // analysis just passes it through. |
| h.thisType = 'C'; |
| h.addMember('C', '_field', 'Object?', |
| whyNotPromotable: PropertyNonPromotabilityReason.isNotFinal); |
| h.run([ |
| if_(thisProperty('_field').eq(nullLiteral), [ |
| return_(), |
| ]), |
| thisProperty('_field').whyNotPromoted((reasons) { |
| expect(reasons.keys, unorderedEquals([Type('Object')])); |
| var nonPromotionReason = |
| reasons.values.single as PropertyNotPromotedForInherentReason; |
| expect(nonPromotionReason.whyNotPromotable, |
| PropertyNonPromotabilityReason.isNotFinal); |
| }), |
| ]); |
| }); |
| |
| test('due to conflict', () { |
| h.thisType = 'C'; |
| h.addMember('C', '_field', 'Object?', whyNotPromotable: null); |
| h.run([ |
| if_(thisProperty('_field').eq(nullLiteral), [ |
| return_(), |
| ]), |
| thisProperty('_field').whyNotPromoted((reasons) { |
| expect(reasons.keys, unorderedEquals([Type('Object')])); |
| var nonPromotionReason = reasons.values.single |
| as PropertyNotPromotedForNonInherentReason; |
| expect(nonPromotionReason.fieldPromotionEnabled, true); |
| }), |
| ]); |
| }); |
| }); |
| }); |
| |
| group('Patterns:', () { |
| group('Assignment:', () { |
| group('Demotion', () { |
| test('Demoting', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'int?'), |
| x.nonNullAssert, |
| checkPromoted(x, 'int'), |
| x.pattern().assign(expr('int?')), |
| checkNotPromoted(x), |
| ]); |
| }); |
| |
| test('Non-demoting', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'num?'), |
| x.nonNullAssert, |
| checkPromoted(x, 'num'), |
| x.pattern().assign(expr('int')), |
| checkPromoted(x, 'num'), |
| ]); |
| }); |
| }); |
| |
| group('Schema:', () { |
| test('Not promoted', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'int?'), |
| x.pattern().assign(expr('int').checkSchema('int?')), |
| ]); |
| }); |
| |
| test('Promoted', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'int?'), |
| x.nonNullAssert, |
| checkPromoted(x, 'int'), |
| x.pattern().assign(expr('int').checkSchema('int')), |
| ]); |
| }); |
| }); |
| |
| group('Promotion:', () { |
| test('Type of interest', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'num'), |
| if_(x.is_('int'), []), |
| checkNotPromoted(x), |
| x.pattern().assign(expr('int')), |
| checkPromoted(x, 'int'), |
| ]); |
| }); |
| |
| test('Not a type of interest', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'num'), |
| x.pattern().assign(expr('int')), |
| checkNotPromoted(x), |
| ]); |
| }); |
| |
| test('Promotes matched value', () { |
| // The code below is equivalent to: |
| // int x; |
| // (x && _!) = ... as dynamic; |
| // There should be an "unnecessary !" warning, because the `x` pattern |
| // implicitly promotes the matched value to type `int`. |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'int'), |
| x |
| .pattern() |
| .and(wildcard().nullAssert..errorId = 'NULLASSERT') |
| .assign(expr('dynamic')), |
| ], expectedErrors: { |
| 'matchedTypeIsStrictlyNonNullable(pattern: NULLASSERT, ' |
| 'matchedType: int)' |
| }); |
| }); |
| |
| test('Does not promote scrutinee', () { |
| // The code below is equivalent to: |
| // int x; |
| // dynamic y = ...; |
| // (x && _) = y; |
| // // y is *not* promoted to `int`. |
| // Although the assignment to `x` performs an implicit downcast, we |
| // don't promote `y` because patterns in irrefutable contexts don't |
| // trigger scrutinee promotion. |
| var x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(x, type: 'int'), |
| declare(y, initializer: expr('dynamic')), |
| x.pattern().and(wildcard()..errorId = 'WILDCARD').assign(y), |
| checkNotPromoted(y), |
| ], expectedErrors: { |
| 'unnecessaryWildcardPattern(pattern: WILDCARD, ' |
| 'kind: logicalAndPatternOperand)', |
| }); |
| }); |
| }); |
| |
| test('Definite assignment', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'int'), |
| checkAssigned(x, false), |
| x.pattern().assign(expr('int')), |
| checkAssigned(x, true), |
| ]); |
| }); |
| |
| group('Boolean condition:', () { |
| test('As main pattern', () { |
| var b = Var('b'); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'int?'), |
| declare(b, type: 'bool'), |
| b.pattern().assign(x.notEq(nullLiteral)), |
| if_(b, [ |
| // `x` is promoted because `b` is known to equal `x != null`. |
| checkPromoted(x, 'int'), |
| ]), |
| ]); |
| }); |
| |
| test('As parenthesized pattern', () { |
| var b = Var('b'); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'int?'), |
| declare(b, type: 'bool'), |
| b.pattern().parenthesized.assign(x.notEq(nullLiteral)), |
| if_(b, [ |
| // `x` is promoted because `b` is known to equal `x != null`. |
| checkPromoted(x, 'int'), |
| ]), |
| ]); |
| }); |
| |
| test('As subpattern', () { |
| h.addMember('bool', 'foo', 'bool'); |
| var b = Var('b'); |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'int?'), |
| declare(b, type: 'bool'), |
| objectPattern( |
| requiredType: 'bool', |
| fields: [b.pattern().recordField('foo')]) |
| .assign(x.notEq(nullLiteral)), |
| if_(b, [ |
| // Even though the RHS of the pattern is `x != null`, `x` is not |
| // promoted because the pattern for `b` is in a subpattern |
| // position. |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| }); |
| |
| group('Demonstrated type:', () { |
| test('Subtype of matched value type', () { |
| var x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(x, initializer: expr('(dynamic,)')), |
| declare(y, type: 'int'), |
| recordPattern([y.pattern().recordField()]) |
| .and(wildcard(expectInferredType: '(int,)') |
| ..errorId = 'WILDCARD') |
| .assign(x), |
| checkNotPromoted(x), |
| ], expectedErrors: { |
| 'unnecessaryWildcardPattern(pattern: WILDCARD, ' |
| 'kind: logicalAndPatternOperand)' |
| }); |
| }); |
| |
| test('Supertype of matched value type', () { |
| var x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(x, initializer: expr('(int,)')), |
| declare(y, type: 'num'), |
| recordPattern([y.pattern().recordField()]) |
| .and(wildcard(expectInferredType: '(int,)') |
| ..errorId = 'WILDCARD') |
| .assign(x), |
| checkNotPromoted(x), |
| ], expectedErrors: { |
| 'unnecessaryWildcardPattern(pattern: WILDCARD, ' |
| 'kind: logicalAndPatternOperand)' |
| }); |
| }); |
| }); |
| }); |
| |
| group('Cast pattern:', () { |
| test('Subtype', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object?'), |
| ifCase(x, wildcard(expectInferredType: 'String').as_('String'), [ |
| checkPromoted(x, 'String'), |
| ]), |
| ]); |
| }); |
| |
| test('Supertype', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'num'), |
| ifCase( |
| x, |
| wildcard(expectInferredType: 'Object').as_('Object') |
| ..errorId = 'PATTERN', |
| [ |
| checkNotPromoted(x), |
| ]), |
| ], expectedErrors: { |
| 'matchedTypeIsSubtypeOfRequired(pattern: PATTERN, ' |
| 'matchedType: num, requiredType: Object)', |
| }); |
| }); |
| |
| test('Unrelated type', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'num'), |
| ifCase(x, wildcard(expectInferredType: 'String').as_('String'), [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| |
| test('Inner promotions have no effect', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object?'), |
| ifCase( |
| x, |
| objectPattern(requiredType: 'int', fields: []).as_('num').and( |
| wildcard(expectInferredType: 'num')..errorId = 'WILDCARD'), |
| [ |
| checkPromoted(x, 'num'), |
| ]), |
| ], expectedErrors: { |
| 'unnecessaryWildcardPattern(pattern: WILDCARD, ' |
| 'kind: logicalAndPatternOperand)', |
| }); |
| }); |
| |
| test('Match failure unreachable', () { |
| // Cast patterns don't fail; they throw exceptions. So the "match |
| // failure" code path should be unreachable. |
| h.run([ |
| ifCase(expr('Object?'), wildcard().as_('int'), [ |
| checkReachable(true), |
| ], [ |
| checkReachable(false), |
| ]), |
| ]); |
| }); |
| |
| test("Doesn't demote", () { |
| var x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(x, initializer: expr('Object?')), |
| ifCase( |
| x, |
| wildcard() |
| .as_('int') |
| .and(wildcard().as_('num')..errorId = 'AS_NUM') |
| .and(y.pattern(expectInferredType: 'int')), |
| [ |
| checkPromoted(x, 'int'), |
| ]), |
| ], expectedErrors: { |
| 'matchedTypeIsSubtypeOfRequired(pattern: AS_NUM, matchedType: int, ' |
| 'requiredType: num)' |
| }); |
| }); |
| |
| group('Demonstrated type:', () { |
| test('Subtype of matched value type', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(num,)')), |
| ifCase(x, recordPattern([wildcard().as_('int').recordField()]), [ |
| checkPromoted(x, '(int,)'), |
| ]), |
| ]); |
| }); |
| |
| test('Supertype of matched value type', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(num,)')), |
| ifCase( |
| x, |
| recordPattern([ |
| (wildcard().as_('Object')..errorId = 'CAST').recordField() |
| ]), |
| [ |
| checkNotPromoted(x), |
| ]), |
| ], expectedErrors: { |
| 'matchedTypeIsSubtypeOfRequired(pattern: CAST, matchedType: num, ' |
| 'requiredType: Object)' |
| }); |
| }); |
| |
| test('Unrelated to matched value type', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(num,)')), |
| ifCase(x, recordPattern([wildcard().as_('String').recordField()]), [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| }); |
| |
| test('Error type does not trigger unnecessary cast warning', () { |
| h.run([ |
| ifCase(expr('int'), wildcard().as_('error'), []), |
| ]); |
| }); |
| |
| test('Promotable property', () { |
| h.addMember('C', '_property', 'int?', promotable: true); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| ifCase(c.property('_property'), wildcard().as_('int'), [ |
| checkPromoted(c.property('_property'), 'int'), |
| ]), |
| ]); |
| }); |
| |
| test('Promotable property, target changed', () { |
| h.addMember('C', '_property', 'Object', promotable: true); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| switch_(c.property('_property'), [ |
| wildcard().as_('num').when(expr('bool')).then([ |
| checkPromoted(c.property('_property'), 'num'), |
| ]), |
| wildcard().when(second(c.write(expr('C')), expr('bool'))).then([]), |
| wildcard().as_('int').then([ |
| checkNotPromoted(c.property('_property')), |
| ]), |
| ]), |
| ]); |
| }); |
| |
| test('Non-promotable property', () { |
| h.addMember('C', '_property', 'int?', promotable: false); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| ifCase(c.property('_property'), wildcard().as_('int'), [ |
| checkNotPromoted(c.property('_property')), |
| ]), |
| ]); |
| }); |
| }); |
| |
| group('Constant pattern:', () { |
| test('Guaranteed match due to Null type', () { |
| h.run([ |
| ifCase(expr('Null'), nullLiteral.pattern, [ |
| checkReachable(true), |
| ], [ |
| checkReachable(false), |
| ]) |
| ]); |
| }); |
| |
| test('Not guaranteed to match due to Null type with old language version', |
| () { |
| h.disablePatterns(); |
| h.run([ |
| switch_( |
| expr('Null'), |
| [ |
| nullLiteral.pattern.then([ |
| checkReachable(true), |
| break_(), |
| ]), |
| default_.then([ |
| checkReachable(true), |
| break_(), |
| ]), |
| ], |
| isLegacyExhaustive: true), |
| ]); |
| }); |
| |
| test('In the general case, may or may not match', () { |
| h.run([ |
| ifCase(expr('Object?'), intLiteral(0).pattern, [ |
| checkReachable(true), |
| ], [ |
| checkReachable(true), |
| ]), |
| ]); |
| }); |
| |
| test('Null pattern promotes unchanged scrutinee', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('int?')), |
| ifCase(x, nullLiteral.pattern, [ |
| checkReachable(true), |
| checkNotPromoted(x), |
| ], [ |
| checkReachable(true), |
| checkPromoted(x, 'int'), |
| ]) |
| ]); |
| }); |
| |
| test("Null pattern doesn't promote scrutinee with old language version", |
| () { |
| h.disablePatterns(); |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('int?')), |
| switch_( |
| x, |
| [ |
| nullLiteral.pattern.then([ |
| checkReachable(true), |
| checkNotPromoted(x), |
| break_(), |
| ]), |
| default_.then([ |
| checkReachable(true), |
| checkNotPromoted(x), |
| break_(), |
| ]), |
| ], |
| isLegacyExhaustive: true), |
| ]); |
| }); |
| |
| test("Null pattern doesn't promote changed scrutinee", () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('int?')), |
| switch_(x, [ |
| wildcard().when(second(x.write(expr('int?')), expr('bool'))).then([ |
| break_(), |
| ]), |
| nullLiteral.pattern.then([ |
| checkReachable(true), |
| checkNotPromoted(x), |
| ]), |
| wildcard(expectInferredType: 'int').then([ |
| checkReachable(true), |
| checkNotPromoted(x), |
| ]), |
| ]), |
| ]); |
| }); |
| |
| test('Null pattern promotes matched pattern var', () { |
| h.run([ |
| ifCase(expr('int?'), |
| nullLiteral.pattern.or(wildcard(expectInferredType: 'int')), []), |
| ]); |
| }); |
| |
| test('Null pattern can even match non-nullable types', () { |
| // Due to mixed mode unsoundness, attempting to match `null` to a |
| // non-nullable type can still succeed, so in order to avoid unsoundness |
| // escalation, it's important that the matching case is considered |
| // reachable. |
| h.run([ |
| ifCase(expr('int'), nullLiteral.pattern, [ |
| checkReachable(true), |
| ], [ |
| checkReachable(true), |
| ]), |
| ]); |
| }); |
| |
| test('Demonstrated type', () { |
| // The demonstrated type of a constant pattern is the matched value |
| // type. We don't want to promote to the constant type because doing so |
| // might be unsound if the user overrides `operator==`. |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(Object,)')), |
| ifCase(x, recordPattern([intLiteral(1).pattern.recordField()]), [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| }); |
| |
| group('For-in statement:', () { |
| test('does not promote iterable', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('List<dynamic>')), |
| patternForIn( |
| wildcard(type: 'int'), |
| x, |
| [], |
| ), |
| checkNotPromoted(x), |
| ]); |
| }); |
| }); |
| |
| group('For-in collection element:', () { |
| test('does not promote iterable', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('List<dynamic>')), |
| listLiteral(elementType: 'Object', [ |
| patternForInElement( |
| wildcard(type: 'int'), |
| x, |
| expr('Object'), |
| ), |
| ]), |
| checkNotPromoted(x), |
| ]); |
| }); |
| }); |
| |
| group('If-case element:', () { |
| test('guarded', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'int?'), |
| listLiteral(elementType: 'String', [ |
| ifCaseElement( |
| expr('Object'), |
| wildcard().when(x.notEq(nullLiteral)), |
| second( |
| listLiteral(elementType: 'dynamic', [ |
| checkReachable(true), |
| checkPromoted(x, 'int'), |
| ]), |
| expr('String')), |
| second( |
| listLiteral(elementType: 'dynamic', [ |
| checkReachable(true), |
| checkNotPromoted(x), |
| ]), |
| expr('String'))), |
| ]), |
| ]); |
| }); |
| |
| test('promotes', () { |
| var x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(x, type: 'num'), |
| listLiteral(elementType: 'String', [ |
| ifCaseElement( |
| x, |
| y.pattern(type: 'int'), |
| second( |
| listLiteral(elementType: 'dynamic', [ |
| checkReachable(true), |
| checkPromoted(x, 'int'), |
| ]), |
| expr('String')), |
| second( |
| listLiteral(elementType: 'dynamic', [ |
| checkReachable(true), |
| checkNotPromoted(x), |
| ]), |
| expr('String'))), |
| ]), |
| ]); |
| }); |
| }); |
| |
| group('If-case statement:', () { |
| test('guarded', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'int?'), |
| ifCase(expr('Object'), wildcard().when(x.notEq(nullLiteral)), [ |
| checkReachable(true), |
| checkPromoted(x, 'int'), |
| ], [ |
| checkReachable(true), |
| checkNotPromoted(x), |
| ]) |
| ]); |
| }); |
| |
| test('promotes', () { |
| var x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(x, type: 'num'), |
| ifCase(x, y.pattern(type: 'int'), [ |
| checkReachable(true), |
| checkPromoted(x, 'int'), |
| ], [ |
| checkReachable(true), |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| |
| test('promotion in both pattern and guard', () { |
| var x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(x, type: 'int?'), |
| declare(y, type: 'String?'), |
| ifCase(x, wildcard(type: 'int').when(y.notEq(nullLiteral)), [ |
| checkReachable(true), |
| checkPromoted(x, 'int'), |
| checkPromoted(y, 'String'), |
| ], [ |
| checkReachable(true), |
| checkNotPromoted(x), |
| checkNotPromoted(y), |
| ]), |
| ]); |
| }); |
| }); |
| |
| group('Logical-and pattern:', () { |
| group('promotion of matched value type:', () { |
| test('when scrutinee is promotable', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'num'), |
| ifCase( |
| x, |
| wildcard(type: 'int').and( |
| wildcard(expectInferredType: 'int')..errorId = 'WILDCARD'), |
| [ |
| checkPromoted(x, 'int'), |
| ]), |
| ], expectedErrors: { |
| 'unnecessaryWildcardPattern(pattern: WILDCARD, ' |
| 'kind: logicalAndPatternOperand)', |
| }); |
| }); |
| |
| test('when scrutinee is not promotable', () { |
| h.run([ |
| ifCase( |
| expr('num'), |
| wildcard(type: 'int').and( |
| wildcard(expectInferredType: 'int')..errorId = 'WILDCARD'), |
| []), |
| ], expectedErrors: { |
| 'unnecessaryWildcardPattern(pattern: WILDCARD, ' |
| 'kind: logicalAndPatternOperand)', |
| }); |
| }); |
| }); |
| |
| test('double promotion of matched value type', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| ifCase( |
| x, |
| wildcard(type: 'num').and(wildcard(type: 'int').and( |
| wildcard(expectInferredType: 'int')..errorId = 'WILDCARD')), |
| [ |
| checkPromoted(x, 'int'), |
| ]), |
| ], expectedErrors: { |
| 'unnecessaryWildcardPattern(pattern: WILDCARD, ' |
| 'kind: logicalAndPatternOperand)', |
| }); |
| }); |
| |
| group('Demonstrated type:', () { |
| test('LHS <: RHS, both could promote', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(Object,)')), |
| ifCase( |
| x, |
| recordPattern([ |
| wildcard(type: 'int') |
| .and(wildcard(type: 'num')..errorId = 'NUM') |
| .recordField() |
| ]), |
| [ |
| checkPromoted(x, '(int,)'), |
| ]), |
| ], expectedErrors: { |
| 'unnecessaryWildcardPattern(pattern: NUM, ' |
| 'kind: logicalAndPatternOperand)' |
| }); |
| }); |
| |
| test('RHS <: LHS, both could promote', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(Object,)')), |
| ifCase( |
| x, |
| recordPattern([ |
| wildcard(type: 'num').and(wildcard(type: 'int')).recordField() |
| ]), |
| [ |
| checkPromoted(x, '(int,)'), |
| ]), |
| ]); |
| }); |
| |
| test('LHS <: RHS, RHS == declared type', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(num,)')), |
| ifCase( |
| x, |
| recordPattern([ |
| wildcard(type: 'int') |
| .and(wildcard(type: 'num')..errorId = 'NUM') |
| .recordField() |
| ]), |
| [ |
| checkPromoted(x, '(int,)'), |
| ]), |
| ], expectedErrors: { |
| 'unnecessaryWildcardPattern(pattern: NUM, ' |
| 'kind: logicalAndPatternOperand)' |
| }); |
| }); |
| |
| test('RHS <: LHS, LHS == declared type', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(num,)')), |
| ifCase( |
| x, |
| recordPattern([ |
| (wildcard(type: 'num')..errorId = 'NUM') |
| .and(wildcard(type: 'int')) |
| .recordField() |
| ]), |
| [ |
| checkPromoted(x, '(int,)'), |
| ]), |
| ], expectedErrors: { |
| 'unnecessaryWildcardPattern(pattern: NUM, ' |
| 'kind: logicalAndPatternOperand)' |
| }); |
| }); |
| |
| test('LHS <: RHS, only LHS could promote', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(num,)')), |
| ifCase( |
| x, |
| recordPattern([ |
| wildcard(type: 'int') |
| .and(wildcard(type: 'Object')..errorId = 'OBJECT') |
| .recordField() |
| ]), |
| [ |
| checkPromoted(x, '(int,)'), |
| ]), |
| ], expectedErrors: { |
| 'unnecessaryWildcardPattern(pattern: OBJECT, ' |
| 'kind: logicalAndPatternOperand)' |
| }); |
| }); |
| |
| test('RHS <: LHS, only RHS could promote', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(num,)')), |
| ifCase( |
| x, |
| recordPattern([ |
| (wildcard(type: 'Object')..errorId = 'OBJECT') |
| .and(wildcard(type: 'int')) |
| .recordField() |
| ]), |
| [ |
| checkPromoted(x, '(int,)'), |
| ]), |
| ], expectedErrors: { |
| 'unnecessaryWildcardPattern(pattern: OBJECT, ' |
| 'kind: logicalAndPatternOperand)' |
| }); |
| }); |
| |
| test('LHS <: RHS, neither could promote', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(int,)')), |
| ifCase( |
| x, |
| recordPattern([ |
| (wildcard(type: 'num')..errorId = 'NUM') |
| .and(wildcard(type: 'Object')..errorId = 'OBJECT') |
| .recordField() |
| ]), |
| [ |
| checkNotPromoted(x), |
| ]), |
| ], expectedErrors: { |
| 'unnecessaryWildcardPattern(pattern: NUM, ' |
| 'kind: logicalAndPatternOperand)', |
| 'unnecessaryWildcardPattern(pattern: OBJECT, ' |
| 'kind: logicalAndPatternOperand)' |
| }); |
| }); |
| |
| test('RHS <: LHS, neither could promote', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(int,)')), |
| ifCase( |
| x, |
| recordPattern([ |
| (wildcard(type: 'Object')..errorId = 'OBJECT') |
| .and(wildcard(type: 'num')..errorId = 'NUM') |
| .recordField() |
| ]), |
| [ |
| checkNotPromoted(x), |
| ]), |
| ], expectedErrors: { |
| 'unnecessaryWildcardPattern(pattern: OBJECT, ' |
| 'kind: logicalAndPatternOperand)', |
| 'unnecessaryWildcardPattern(pattern: NUM, ' |
| 'kind: logicalAndPatternOperand)' |
| }); |
| }); |
| }); |
| }); |
| |
| group('Logical-or pattern:', () { |
| group('Joins promotions of scrutinee:', () { |
| test('LHS more promoted', () { |
| var x = Var('x'); |
| // `(num() && int()) || num()` retains promotion to `num` |
| h.run([ |
| declare(x, initializer: expr('Object')), |
| ifCase( |
| x, |
| objectPattern(requiredType: 'num', fields: []) |
| .and(objectPattern(requiredType: 'int', fields: [])) |
| .or(objectPattern(requiredType: 'num', fields: [])), |
| [ |
| checkPromoted(x, 'num'), |
| ]), |
| ]); |
| }); |
| |
| test('RHS more promoted', () { |
| var x = Var('x'); |
| // `num() || (num() && int())` retains promotion to `num` |
| h.run([ |
| declare(x, initializer: expr('Object')), |
| ifCase( |
| x, |
| objectPattern(requiredType: 'num', fields: []).or( |
| objectPattern(requiredType: 'num', fields: []) |
| .and(objectPattern(requiredType: 'int', fields: []))), |
| [ |
| checkPromoted(x, 'num'), |
| ]), |
| ]); |
| }); |
| }); |
| |
| group('Joins promotions of implicit temporary match variable:', () { |
| test('LHS more promoted', () { |
| // `(num() && int()) || num()` retains promotion to `num` |
| h.run([ |
| ifCase( |
| expr('Object'), |
| objectPattern(requiredType: 'num', fields: []) |
| .and(objectPattern(requiredType: 'int', fields: [])) |
| .or(objectPattern(requiredType: 'num', fields: [])) |
| .and(wildcard(expectInferredType: 'num') |
| ..errorId = 'WILDCARD'), |
| []), |
| ], expectedErrors: { |
| 'unnecessaryWildcardPattern(pattern: WILDCARD, ' |
| 'kind: logicalAndPatternOperand)', |
| }); |
| }); |
| |
| test('RHS more promoted', () { |
| // `num() || (num() && int())` retains promotion to `num` |
| h.run([ |
| ifCase( |
| expr('Object'), |
| objectPattern(requiredType: 'num', fields: []) |
| .or(objectPattern(requiredType: 'num', fields: []) |
| .and(objectPattern(requiredType: 'int', fields: []))) |
| .and(wildcard(expectInferredType: 'num') |
| ..errorId = 'WILDCARD'), |
| []), |
| ], expectedErrors: { |
| 'unnecessaryWildcardPattern(pattern: WILDCARD, ' |
| 'kind: logicalAndPatternOperand)', |
| }); |
| }); |
| }); |
| |
| group('Joins explicitly declared variables:', () { |
| test('LHS promoted', () { |
| var x1 = Var('x', identity: 'x1'); |
| var x2 = Var('x', identity: 'x2'); |
| var x = PatternVariableJoin('x', expectedComponents: [x1, x2]); |
| h.run([ |
| ifCase( |
| expr('int?'), |
| x1.pattern(type: 'int?').nullCheck.or(x2.pattern(type: 'int?')), |
| [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| |
| test('RHS promoted', () { |
| var x1 = Var('x', identity: 'x1'); |
| var x2 = Var('x', identity: 'x2'); |
| var x = PatternVariableJoin('x', expectedComponents: [x1, x2]); |
| h.run([ |
| ifCase( |
| expr('int?'), |
| x1.pattern(type: 'int?').or(x2.pattern(type: 'int?').nullCheck), |
| [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| |
| test('Both sides promoted', () { |
| var x1 = Var('x', identity: 'x1'); |
| var x2 = Var('x', identity: 'x2'); |
| var x = PatternVariableJoin('x', expectedComponents: [x1, x2]); |
| h.run([ |
| ifCase( |
| expr('int?'), |
| x1 |
| .pattern(type: 'int?') |
| .nullCheck |
| .or(x2.pattern(type: 'int?').nullCheck), |
| [ |
| checkPromoted(x, 'int'), |
| ]), |
| ]); |
| }); |
| |
| test('Join variable is promotable', () { |
| var x1 = Var('x', identity: 'x1'); |
| var x2 = Var('x', identity: 'x2'); |
| var x = PatternVariableJoin('x', expectedComponents: [x1, x2]); |
| h.run([ |
| ifCase( |
| expr('int?'), |
| x1.pattern(type: 'int?').nullCheck.or(x2.pattern(type: 'int?')), |
| [ |
| checkNotPromoted(x), |
| x.nonNullAssert, |
| checkPromoted(x, 'int'), |
| ]), |
| ]); |
| }); |
| }); |
| |
| group( |
| 'Sets join variable assigned even if variable appears on only one ' |
| 'side:', () { |
| test('Variable on LHS only', () { |
| var x1 = Var('x', identity: 'x1')..errorId = 'X1'; |
| var x = PatternVariableJoin('x', expectedComponents: [x1]); |
| // `x` is considered assigned inside the `true` branch (even though |
| // it's not actually assigned on both sides of the or-pattern) because |
| // this avoids redundant errors. |
| h.run([ |
| ifCase(expr('num?'), |
| (x1.pattern().nullCheck.or(wildcard()))..errorId = 'OR', [ |
| checkAssigned(x, true), |
| // Also verify that the join variable is promotable |
| checkNotPromoted(x), |
| x.as_('int'), |
| checkPromoted(x, 'int'), |
| ]), |
| ], expectedErrors: { |
| 'logicalOrPatternBranchMissingVariable(node: OR, hasInLeft: true, ' |
| 'name: x, variable: X1)' |
| }); |
| }); |
| |
| test('Variable on RHS only', () { |
| var x1 = Var('x', identity: 'x1')..errorId = 'X1'; |
| var x = PatternVariableJoin('x', expectedComponents: [x1]); |
| // `x` is considered assigned inside the `true` branch (even though |
| // it's not actually assigned on both sides of the or-pattern) because |
| // this avoids redundant errors. |
| h.run([ |
| ifCase(expr('int?'), |
| (wildcard().nullCheck.or(x1.pattern()))..errorId = 'OR', [ |
| checkAssigned(x, true), |
| // Also verify that the join variable is promotable |
| checkNotPromoted(x), |
| x.nonNullAssert, |
| checkPromoted(x, 'int'), |
| ]), |
| ], expectedErrors: { |
| 'logicalOrPatternBranchMissingVariable(node: OR, hasInLeft: false, ' |
| 'name: x, variable: X1)' |
| }); |
| }); |
| }); |
| |
| group('Demonstrated type:', () { |
| test('LHS <: RHS, both could promote', () { |
| // In the circumstance where the LHS and RHS of the logical-or pattern |
| // promote the matched value to different types, we don't retain any |
| // promotion. This is similar to how we don't retain any promotion |
| // for a test like `if (x is int || x is num)`. |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(Object,)')), |
| ifCase( |
| x, |
| recordPattern([ |
| wildcard(type: 'int').or(wildcard(type: 'num')).recordField() |
| ]), |
| [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| |
| test('RHS <: LHS, both could promote', () { |
| // In the circumstance where the LHS and RHS of the logical-or pattern |
| // promote the matched value to different types, we don't retain any |
| // promotion. This is similar to how we don't retain any promotion |
| // for a test like `if (x is int || x is num)`. |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(Object,)')), |
| ifCase( |
| x, |
| recordPattern([ |
| wildcard(type: 'num').or(wildcard(type: 'int')).recordField() |
| ]), |
| [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| |
| test('LHS == RHS, could promote', () { |
| // In the circumstance where the LHS and RHS of the logical-or pattern |
| // promote the matched value to the same type, we do retain the |
| // promotion. This is similar to how we retain the promotion for a |
| // test like `if (x is num || x is num)`. |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(Object,)')), |
| ifCase( |
| x, |
| recordPattern([ |
| wildcard(type: 'num').or(wildcard(type: 'num')).recordField() |
| ]), |
| [ |
| checkPromoted(x, '(num,)'), |
| ]), |
| ]); |
| }); |
| |
| test('LHS <: RHS, only LHS could promote', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(num,)')), |
| ifCase( |
| x, |
| recordPattern([ |
| wildcard(type: 'int') |
| .or(wildcard(type: 'Object')) |
| .recordField() |
| ]), |
| [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| |
| test('RHS <: LHS, only RHS could promote', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(num,)')), |
| ifCase( |
| x, |
| recordPattern([ |
| wildcard(type: 'Object') |
| .or(wildcard(type: 'int')) |
| .recordField() |
| ]), |
| [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| |
| test('LHS <: RHS, neither could promote', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(int,)')), |
| ifCase( |
| x, |
| recordPattern([ |
| wildcard(type: 'num') |
| .or(wildcard(type: 'Object')) |
| .recordField() |
| ]), |
| [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| |
| test('RHS <: LHS, neither could promote', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(int,)')), |
| ifCase( |
| x, |
| recordPattern([ |
| wildcard(type: 'Object') |
| .or(wildcard(type: 'num')) |
| .recordField() |
| ]), |
| [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| |
| test('Does not promote to LUB', () { |
| // `if (x case (int _ || double _,)` doesn't promote `x` to `(num,)`. |
| // Rationale: we want to be consistent with the behavior of |
| // `if (x case int _ || double _)`, which doesn't promote to `num`. |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(Object?,)')), |
| ifCase( |
| x, |
| recordPattern([ |
| wildcard(type: 'int') |
| .or(wildcard(type: 'double')) |
| .recordField() |
| ]), |
| [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| }); |
| }); |
| |
| group('List pattern:', () { |
| group('Not guaranteed to match:', () { |
| group('Empty list:', () { |
| test('matched value type is non-nullable list', () { |
| h.run([ |
| switch_(expr('List<Object>'), [ |
| listPattern([]).then([break_()]), |
| default_.then([ |
| checkReachable(true), |
| ]), |
| ]), |
| ]); |
| }); |
| |
| test('matched value type is nullable list', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('List<Object?>?')), |
| switch_(x, [ |
| listPattern([]).then([ |
| checkReachable(true), |
| checkPromoted(x, 'List<Object?>'), |
| ]), |
| default_.then([ |
| checkReachable(true), |
| checkNotPromoted(x), |
| ]), |
| ]), |
| ]); |
| }); |
| }); |
| |
| test('Single non-rest element', () { |
| h.run([ |
| switch_(expr('List<Object>'), [ |
| listPattern([wildcard()]).then([break_()]), |
| default_.then([ |
| checkReachable(true), |
| ]), |
| ]), |
| ]); |
| }); |
| |
| test('Rest pattern with subpattern that may fail to match', () { |
| h.run([ |
| switch_(expr('List<Object>'), [ |
| listPattern([restPattern(listPattern([]))]).then([break_()]), |
| default_.then([ |
| checkReachable(true), |
| ]) |
| ]) |
| ]); |
| }); |
| }); |
| |
| group('Guaranteed to match:', () { |
| test('Rest pattern with no subpattern', () { |
| h.run([ |
| switch_(expr('List<Object>'), [ |
| listPattern([restPattern()]).then([break_()]), |
| default_.then([ |
| checkReachable(false), |
| ]) |
| ]) |
| ]); |
| }); |
| |
| test('Rest pattern with subpattern that always matches', () { |
| h.run([ |
| switch_(expr('List<Object>'), [ |
| listPattern([restPattern(wildcard())]).then([break_()]), |
| default_.then([ |
| checkReachable(false), |
| ]) |
| ]) |
| ]); |
| }); |
| }); |
| |
| test('Promotes', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('Object?')), |
| ifCase(x, listPattern([wildcard()], elementType: 'int'), [ |
| checkPromoted(x, 'List<int>'), |
| ]), |
| ]); |
| }); |
| |
| test("Doesn't demote", () { |
| var x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(x, initializer: expr('Object?')), |
| ifCase( |
| x, |
| wildcard() |
| .as_('List<int>') |
| .and(listPattern([], elementType: 'num')) |
| .and(y.pattern(expectInferredType: 'List<int>')), |
| [ |
| checkPromoted(x, 'List<int>'), |
| ]), |
| ]); |
| }); |
| |
| test('Reachability', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('List<int>')), |
| ifCase(x, listPattern([], elementType: 'int'), [ |
| checkReachable(true), |
| ], [ |
| checkReachable(true), |
| ]), |
| ]); |
| }); |
| |
| group('Demonstrated type:', () { |
| test('Subtype of matched value type', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(Iterable<Object>,)')), |
| ifCase( |
| x, |
| recordPattern([ |
| listPattern([wildcard(type: 'int')], elementType: 'num') |
| .recordField() |
| ]), |
| [ |
| checkPromoted(x, '(List<num>,)'), |
| ]), |
| ]); |
| }); |
| |
| test('Supertype of matched value type', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(List<num>,)')), |
| ifCase( |
| x, |
| recordPattern([ |
| listPattern([wildcard(type: 'int')], elementType: 'Object') |
| .recordField() |
| ]), |
| [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| |
| test('Unrelated to matched value type', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(List<int?>,)')), |
| ifCase( |
| x, |
| recordPattern([ |
| listPattern([wildcard(type: 'int')], elementType: 'num') |
| .recordField() |
| ]), |
| [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| }); |
| |
| test('Promotable property', () { |
| h.addMember('C', '_property', 'Object', promotable: true); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| ifCase(c.property('_property'), listPattern([]), [ |
| checkPromoted(c.property('_property'), 'List<Object?>'), |
| ]), |
| ]); |
| }); |
| |
| test('Promotable property, target changed', () { |
| h.addMember('C', '_property', 'Object', promotable: true); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| switch_(c.property('_property'), [ |
| listPattern([]).when(expr('bool')).then([ |
| checkPromoted(c.property('_property'), 'List<Object?>'), |
| ]), |
| wildcard().when(second(c.write(expr('C')), expr('bool'))).then([]), |
| listPattern([]).then([ |
| checkNotPromoted(c.property('_property')), |
| ]), |
| ]), |
| ]); |
| }); |
| |
| test('Non-promotable property', () { |
| h.addMember('C', '_property', 'Object', promotable: false); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| ifCase(c.property('_property'), listPattern([]), [ |
| checkNotPromoted(c.property('_property')), |
| ]), |
| ]); |
| }); |
| }); |
| |
| group('Map pattern:', () { |
| test('Promotes', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('Object?')), |
| ifCase( |
| x, |
| mapPattern([mapPatternEntry(intLiteral(0), wildcard())], |
| keyType: 'int', valueType: 'String'), |
| [ |
| checkPromoted(x, 'Map<int, String>'), |
| ]), |
| ]); |
| }); |
| |
| test('Match failure reachable', () { |
| h.run([ |
| ifCase( |
| expr('Object?'), |
| mapPattern([ |
| mapPatternEntry(expr('Object'), wildcard()), |
| ]), |
| [ |
| checkReachable(true), |
| ], |
| [ |
| checkReachable(true), |
| ]), |
| ]); |
| }); |
| |
| test("Doesn't demote", () { |
| var x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(x, initializer: expr('Object?')), |
| ifCase( |
| x, |
| wildcard() |
| .as_('Map<int, int>') |
| .and(mapPattern([ |
| mapPatternEntry(expr('Object'), wildcard()), |
| ], keyType: 'num', valueType: 'num')) |
| .and(y.pattern(expectInferredType: 'Map<int, int>')), |
| [ |
| checkPromoted(x, 'Map<int, int>'), |
| ]), |
| ]); |
| }); |
| |
| test('Reachability', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('Map<int, int>')), |
| ifCase( |
| x, |
| mapPattern([ |
| mapPatternEntry(expr('Object'), wildcard()), |
| ], keyType: 'int', valueType: 'int'), |
| [ |
| checkReachable(true), |
| ], |
| [ |
| checkReachable(true), |
| ]), |
| ]); |
| }); |
| |
| group('Demonstrated type:', () { |
| test('Subtype of matched value type', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(Map<num?, Object>?,)')), |
| ifCase( |
| x, |
| recordPattern([ |
| mapPattern([ |
| mapPatternEntry(intLiteral(0), wildcard(type: 'int')) |
| ], keyType: 'int?', valueType: 'num') |
| .recordField() |
| ]), |
| [ |
| checkPromoted(x, '(Map<int?, num>,)'), |
| ]), |
| ]); |
| }); |
| |
| test('Supertype of matched value type', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(Map<int, num>,)')), |
| ifCase( |
| x, |
| recordPattern([ |
| mapPattern([ |
| mapPatternEntry(intLiteral(0), wildcard(type: 'int')) |
| ], keyType: 'int', valueType: 'Object') |
| .recordField() |
| ]), |
| [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| |
| test('Unrelated to matched value type', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(Map<int, int?>,)')), |
| ifCase( |
| x, |
| recordPattern([ |
| mapPattern([ |
| mapPatternEntry(intLiteral(0), wildcard(type: 'int')) |
| ], keyType: 'int', valueType: 'num') |
| .recordField() |
| ]), |
| [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| }); |
| |
| test('Promotable property', () { |
| h.addMember('C', '_property', 'Object', promotable: true); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| ifCase(c.property('_property'), |
| mapPattern([mapPatternEntry(intLiteral(0), wildcard())]), [ |
| checkPromoted(c.property('_property'), 'Map<Object?, Object?>'), |
| ]), |
| ]); |
| }); |
| |
| test('Promotable property, target changed', () { |
| h.addMember('C', '_property', 'Object', promotable: true); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| switch_(c.property('_property'), [ |
| mapPattern([mapPatternEntry(intLiteral(0), wildcard())]) |
| .when(expr('bool')) |
| .then([ |
| checkPromoted(c.property('_property'), 'Map<Object?, Object?>'), |
| ]), |
| wildcard().when(second(c.write(expr('C')), expr('bool'))).then([]), |
| mapPattern([mapPatternEntry(intLiteral(0), wildcard())]).then([ |
| checkNotPromoted(c.property('_property')), |
| ]), |
| ]), |
| ]); |
| }); |
| |
| test('Non-promotable property', () { |
| h.addMember('C', '_property', 'Object', promotable: false); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| ifCase(c.property('_property'), |
| mapPattern([mapPatternEntry(intLiteral(0), wildcard())]), [ |
| checkNotPromoted(c.property('_property')), |
| ]), |
| ]); |
| }); |
| }); |
| |
| group('Null-assert:', () { |
| test('Throws if not null', () { |
| h.run([ |
| ifCase(expr('Object?'), wildcard().nullAssert, [], [ |
| checkReachable(false), |
| ]), |
| ]); |
| }); |
| |
| group('Scrutinee promotion:', () { |
| test('If changed', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('Object?')), |
| switch_(x, [ |
| wildcard() |
| .when(second(x.write(expr('Object?')), expr('bool'))) |
| .then([ |
| break_(), |
| ]), |
| wildcard().nullAssert.then([ |
| checkNotPromoted(x), |
| ]) |
| ]), |
| ]); |
| }); |
| |
| test('If unchanged', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('Object?')), |
| ifCase(x, wildcard().nullAssert, [ |
| checkPromoted(x, 'Object'), |
| ], [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| |
| test('If subpattern', () { |
| // Equivalent Dart code: |
| // typedef T = int?; |
| // extension on T { |
| // dynamic get foo { ... } |
| // } |
| // f(Object? x) { |
| // if (x case T(foo: _!)) { |
| // // x still might be `null` |
| // } |
| // } |
| h.addDownwardInfer(name: 'T', context: 'Object?', result: 'int?'); |
| h.addMember('int?', 'foo', 'dynamic'); |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('Object?')), |
| ifCase( |
| x, |
| objectPattern( |
| requiredType: 'T', |
| fields: [wildcard().nullAssert.recordField('foo')]), |
| [ |
| checkPromoted(x, 'int?'), |
| ]), |
| ]); |
| }); |
| |
| test('If promotable property', () { |
| h.addMember('C', '_property', 'int?', promotable: true); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| ifCase(c.property('_property'), wildcard().nullAssert, [ |
| checkPromoted(c.property('_property'), 'int'), |
| ]), |
| ]); |
| }); |
| |
| test('If promotable property, target changed', () { |
| h.addMember('C', '_property', 'int?', promotable: true); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| switch_(c.property('_property'), [ |
| wildcard().nullAssert.when(expr('bool')).then([ |
| checkPromoted(c.property('_property'), 'int'), |
| ]), |
| wildcard() |
| .when(second(c.write(expr('C')), expr('bool'))) |
| .then([]), |
| (wildcard().nullAssert..errorId = 'SECOND_NULL_ASSERT').then([ |
| checkNotPromoted(c.property('_property')), |
| ]), |
| ]), |
| ], expectedErrors: { |
| 'matchedTypeIsStrictlyNonNullable(' |
| 'pattern: SECOND_NULL_ASSERT, matchedType: int)', |
| }); |
| }); |
| |
| test('If non-promotable property', () { |
| h.addMember('C', '_property', 'int?', promotable: false); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| ifCase(c.property('_property'), wildcard().nullAssert, [ |
| checkNotPromoted(c.property('_property')), |
| ]), |
| ]); |
| }); |
| }); |
| |
| test('Promotes temporary variable', () { |
| h.run([ |
| ifCase( |
| expr('Object?'), |
| wildcard().nullAssert.and( |
| wildcard(expectInferredType: 'Object')..errorId = 'WILDCARD'), |
| []), |
| ], expectedErrors: { |
| 'unnecessaryWildcardPattern(pattern: WILDCARD, ' |
| 'kind: logicalAndPatternOperand)' |
| }); |
| }); |
| |
| test('Unreachable if null', () { |
| h.run([ |
| ifCase(expr('Null'), wildcard().nullAssert, [ |
| checkReachable(false), |
| ]), |
| ]); |
| }); |
| |
| test('Reachable otherwise', () { |
| h.run([ |
| ifCase(expr('Object?'), wildcard().nullAssert, [ |
| checkReachable(true), |
| ]), |
| ]); |
| }); |
| |
| test("Doesn't demote", () { |
| var x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(x, initializer: expr('Object?')), |
| ifCase( |
| x, |
| wildcard() |
| .as_('int?') |
| .and(wildcard().nullAssert) |
| .and(y.pattern(expectInferredType: 'int')), |
| [ |
| checkPromoted(x, 'int'), |
| ]), |
| ]); |
| }); |
| |
| test('Demonstrated type', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(int?,)')), |
| ifCase(x, recordPattern([wildcard().nullAssert.recordField()]), [ |
| checkPromoted(x, '(int,)'), |
| ]), |
| ]); |
| }); |
| }); |
| |
| group('Null-check:', () { |
| test('Might not match', () { |
| h.run([ |
| ifCase(expr('Object?'), wildcard().nullCheck, [], [ |
| checkReachable(true), |
| ]), |
| ]); |
| }); |
| |
| group('Scrutinee promotion:', () { |
| test('If changed', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('Object?')), |
| switch_(x, [ |
| wildcard() |
| .when(second(x.write(expr('Object?')), expr('bool'))) |
| .then([ |
| break_(), |
| ]), |
| wildcard().nullCheck.then([ |
| checkNotPromoted(x), |
| ]) |
| ]), |
| ]); |
| }); |
| |
| test('If unchanged', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('Object?')), |
| ifCase(x, wildcard().nullCheck, [ |
| checkPromoted(x, 'Object'), |
| ], [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| |
| test('If subpattern', () { |
| // Equivalent Dart code: |
| // typedef T = int?; |
| // extension on T { |
| // dynamic get foo { ... } |
| // } |
| // f(Object? x) { |
| // if (x case T(foo: _?)) { |
| // // x still might be `null` |
| // } |
| // } |
| h.addDownwardInfer(name: 'T', context: 'Object?', result: 'int?'); |
| h.addMember('int?', 'foo', 'dynamic'); |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('Object?')), |
| ifCase( |
| x, |
| objectPattern( |
| requiredType: 'T', |
| fields: [wildcard().nullCheck.recordField('foo')]), |
| [ |
| checkPromoted(x, 'int?'), |
| ]), |
| ]); |
| }); |
| |
| test('If promotable property', () { |
| h.addMember('C', '_property', 'int?', promotable: true); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| ifCase(c.property('_property'), wildcard().nullCheck, [ |
| checkPromoted(c.property('_property'), 'int'), |
| ]), |
| ]); |
| }); |
| |
| test('If promotable property, target changed', () { |
| h.addMember('C', '_property', 'int?', promotable: true); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| switch_(c.property('_property'), [ |
| wildcard().nullCheck.when(expr('bool')).then([ |
| checkPromoted(c.property('_property'), 'int'), |
| ]), |
| wildcard() |
| .when(second(c.write(expr('C')), expr('bool'))) |
| .then([]), |
| wildcard().nullCheck.then([ |
| checkNotPromoted(c.property('_property')), |
| ]), |
| ]), |
| ]); |
| }); |
| |
| test('If non-promotable property', () { |
| h.addMember('C', '_property', 'int?', promotable: false); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| ifCase(c.property('_property'), wildcard().nullCheck, [ |
| checkNotPromoted(c.property('_property')), |
| ]), |
| ]); |
| }); |
| }); |
| |
| test('Promotes temporary variable', () { |
| h.run([ |
| ifCase( |
| expr('Object?'), |
| wildcard().nullCheck.and( |
| wildcard(expectInferredType: 'Object')..errorId = 'WILDCARD'), |
| []), |
| ], expectedErrors: { |
| 'unnecessaryWildcardPattern(pattern: WILDCARD, ' |
| 'kind: logicalAndPatternOperand)' |
| }); |
| }); |
| |
| test('Unreachable if null', () { |
| h.run([ |
| ifCase(expr('Null'), wildcard().nullCheck, [ |
| checkReachable(false), |
| ]), |
| ]); |
| }); |
| |
| test('Reachable otherwise', () { |
| h.run([ |
| ifCase(expr('Object?'), wildcard().nullCheck, [ |
| checkReachable(true), |
| ]), |
| ]); |
| }); |
| |
| test("Doesn't demote", () { |
| var x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(x, initializer: expr('Object?')), |
| ifCase( |
| x, |
| wildcard() |
| .as_('int?') |
| .and(wildcard().nullCheck) |
| .and(y.pattern(expectInferredType: 'int')), |
| [ |
| checkPromoted(x, 'int'), |
| ]), |
| ]); |
| }); |
| |
| test('Demonstrated type', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(int?,)')), |
| ifCase(x, recordPattern([wildcard().nullCheck.recordField()]), [ |
| checkPromoted(x, '(int,)'), |
| ]), |
| ]); |
| }); |
| }); |
| |
| group('Object pattern:', () { |
| test('Promotes', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('Object?')), |
| ifCase(x, objectPattern(requiredType: 'int', fields: []), [ |
| checkPromoted(x, 'int'), |
| ]), |
| ]); |
| }); |
| |
| test("Doesn't demote", () { |
| var x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(x, initializer: expr('Object?')), |
| ifCase( |
| x, |
| wildcard() |
| .as_('int') |
| .and(objectPattern(requiredType: 'num', fields: [])) |
| .and(y.pattern(expectInferredType: 'int')), |
| [ |
| checkPromoted(x, 'int'), |
| ]), |
| ]); |
| }); |
| |
| group('Demonstrated type:', () { |
| test('Subtype of matched value type', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(num,)')), |
| ifCase( |
| x, |
| recordPattern([ |
| objectPattern(requiredType: 'int', fields: []).recordField() |
| ]), |
| [ |
| checkPromoted(x, '(int,)'), |
| ]), |
| ]); |
| }); |
| |
| test('Supertype of matched value type', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(num,)')), |
| ifCase( |
| x, |
| recordPattern([ |
| objectPattern(requiredType: 'Object', fields: []) |
| .recordField() |
| ]), |
| [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| |
| test('Unrelated to matched value type', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(num,)')), |
| ifCase( |
| x, |
| recordPattern([ |
| objectPattern(requiredType: 'String', fields: []) |
| .recordField() |
| ]), |
| [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| }); |
| |
| test('Read of Never typed getter makes unreachable', () { |
| h.addDownwardInfer(name: 'A', context: 'Object', result: 'A'); |
| h.addMember('A', 'foo', 'Never'); |
| h.run([ |
| ifCase( |
| expr('Object'), |
| objectPattern(requiredType: 'A', fields: [ |
| Var('foo').pattern().recordField('foo'), |
| ]), |
| [ |
| checkReachable(false), |
| ], |
| ), |
| ]); |
| }); |
| |
| test('Promotable property', () { |
| h.addMember('C', '_property', 'Object', promotable: true); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| ifCase(c.property('_property'), |
| objectPattern(requiredType: 'int', fields: []), [ |
| checkPromoted(c.property('_property'), 'int'), |
| ]), |
| ]); |
| }); |
| |
| test('Promotable property, target changed', () { |
| h.addMember('C', '_property', 'Object', promotable: true); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| switch_(c.property('_property'), [ |
| objectPattern(requiredType: 'int', fields: []) |
| .when(expr('bool')) |
| .then([ |
| checkPromoted(c.property('_property'), 'int'), |
| ]), |
| wildcard().when(second(c.write(expr('C')), expr('bool'))).then([]), |
| objectPattern(requiredType: 'int', fields: []).then([ |
| checkNotPromoted(c.property('_property')), |
| ]), |
| ]), |
| ]); |
| }); |
| |
| test('Non-promotable property', () { |
| h.addMember('C', '_property', 'Object', promotable: false); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| ifCase(c.property('_property'), |
| objectPattern(requiredType: 'int', fields: []), [ |
| checkNotPromoted(c.property('_property')), |
| ]), |
| ]); |
| }); |
| }); |
| |
| group('Pattern assignment:', () { |
| test('Does not promote RHS', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('num')), |
| wildcard().as_('int').assign(x), |
| checkNotPromoted(x), |
| ]); |
| }); |
| }); |
| |
| group('Pattern variable declaration:', () { |
| test('Does not promote RHS', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('num')), |
| match(wildcard().as_('int'), x), |
| checkNotPromoted(x), |
| ]); |
| }); |
| }); |
| |
| group('Record pattern:', () { |
| test('Simple promotion', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('Object?')), |
| ifCase(x, recordPattern([wildcard().recordField()]), [ |
| checkPromoted(x, '(Object?,)'), |
| ]), |
| ]); |
| }); |
| |
| group('Promote to demonstrated type:', () { |
| test('Unnamed fields', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('Object?')), |
| ifCase( |
| x, |
| recordPattern([ |
| wildcard(type: 'int').recordField(), |
| wildcard(type: 'String').recordField() |
| ]), |
| [ |
| checkPromoted(x, '(int, String)'), |
| ]), |
| ]); |
| }); |
| |
| test('Named fields', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('Object?')), |
| ifCase( |
| x, |
| recordPattern([ |
| wildcard(type: 'int').recordField('i'), |
| wildcard(type: 'String').recordField('s') |
| ]), |
| [ |
| checkPromoted(x, '({int i, String s})'), |
| ]), |
| ]); |
| }); |
| }); |
| |
| test('Required type is a type of interest', () { |
| // The required type is `(Object?,)`. Since that's the type used in the |
| // desugared type test, it's considered a type of interest even though |
| // the scrutinee is initially promoted to `(int,)`. |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('Object')), |
| ifCase(x, recordPattern([wildcard(type: 'int').recordField()]), [ |
| checkPromoted(x, '(int,)'), |
| x.write(expr('(num,)')), |
| checkPromoted(x, '(Object?,)'), |
| ]), |
| ]); |
| }); |
| |
| test('Promotion to demonstrated type cannot fail', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(Object?,)')), |
| ifCase(x, recordPattern([wildcard().as_('int').recordField()]), [ |
| checkPromoted(x, '(int,)'), |
| ], [ |
| checkReachable( |
| false, |
| ), |
| ]), |
| ]); |
| }); |
| |
| test('Match failure reachable', () { |
| h.run([ |
| ifCase(expr('Object?'), recordPattern([wildcard().recordField()]), [ |
| checkReachable(true), |
| ], [ |
| checkReachable(true), |
| ]), |
| ]); |
| }); |
| |
| test("Doesn't demote", () { |
| var x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(x, initializer: expr('Object?')), |
| ifCase( |
| x, |
| wildcard() |
| .as_('(int,)') |
| .and(recordPattern([wildcard().recordField()])) |
| .and(y.pattern(expectInferredType: '(int,)')), |
| [ |
| checkPromoted(x, '(int,)'), |
| ]), |
| ]); |
| }); |
| |
| group('Demonstrated type:', () { |
| test('Subtype of matched value type', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('((num,),)')), |
| ifCase( |
| x, |
| recordPattern([ |
| recordPattern([wildcard(type: 'int').recordField()]) |
| .recordField() |
| ]), |
| [ |
| checkPromoted(x, '((int,),)'), |
| ]), |
| ]); |
| }); |
| |
| test('Supertype of matched value type', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('Never')), |
| ifCase( |
| x, |
| recordPattern([ |
| recordPattern([wildcard(type: 'num').recordField()]) |
| .recordField() |
| ]), |
| [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| |
| test('Unrelated to matched value type', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('String')), |
| ifCase( |
| x, |
| recordPattern([ |
| recordPattern([wildcard(type: 'num').recordField()]) |
| .recordField() |
| ]), |
| [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| }); |
| |
| test('Error type does not alter previous reachability conclusions', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(Null, Object?)')), |
| ifCase( |
| x, |
| recordPattern([ |
| relationalPattern('!=', nullLiteral).recordField(), |
| wildcard(type: 'error').recordField() |
| ]), |
| [ |
| checkReachable(false), |
| ], |
| [ |
| checkReachable(true), |
| ]), |
| ]); |
| }); |
| |
| test('Promotable property', () { |
| h.addMember('C', '_property', 'Object', promotable: true); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| ifCase(c.property('_property'), recordPattern([]), [ |
| checkPromoted(c.property('_property'), '()'), |
| ]), |
| ]); |
| }); |
| |
| test('Promotable property, target changed', () { |
| h.addMember('C', '_property', 'Object', promotable: true); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| switch_(c.property('_property'), [ |
| recordPattern([]).when(expr('bool')).then([ |
| checkPromoted(c.property('_property'), '()'), |
| ]), |
| wildcard().when(second(c.write(expr('C')), expr('bool'))).then([]), |
| recordPattern([]).then([ |
| checkNotPromoted(c.property('_property')), |
| ]), |
| ]), |
| ]); |
| }); |
| |
| test('Non-promotable property', () { |
| h.addMember('C', '_property', 'Object', promotable: false); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| ifCase(c.property('_property'), recordPattern([]), [ |
| checkNotPromoted(c.property('_property')), |
| ]), |
| ]); |
| }); |
| }); |
| |
| group('Relational pattern:', () { |
| group('==:', () { |
| test('Guaranteed match due to Null type', () { |
| h.run([ |
| ifCase(expr('Null'), relationalPattern('==', nullLiteral), [ |
| checkReachable(true), |
| ], [ |
| checkReachable(false), |
| ]) |
| ]); |
| }); |
| |
| test('Guaranteed match due to Null type in subpattern', () { |
| h.run([ |
| ifCase( |
| expr('(Null,)'), |
| recordPattern( |
| [relationalPattern('==', nullLiteral).recordField()]), |
| [ |
| checkReachable(true), |
| ], |
| [ |
| checkReachable(false), |
| ]) |
| ]); |
| }); |
| |
| test('In the general case, may or may not match', () { |
| h.run([ |
| ifCase(expr('Object?'), relationalPattern('==', intLiteral(0)), [ |
| checkReachable(true), |
| ], [ |
| checkReachable(true), |
| ]), |
| ]); |
| }); |
| |
| test('Null pattern promotes unchanged scrutinee', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('int?')), |
| ifCase(x, relationalPattern('==', nullLiteral), [ |
| checkReachable(true), |
| checkNotPromoted(x), |
| ], [ |
| checkReachable(true), |
| checkPromoted(x, 'int'), |
| ]) |
| ]); |
| }); |
| |
| test("Null pattern doesn't promote changed scrutinee", () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('int?')), |
| switch_(x, [ |
| wildcard() |
| .when(second(x.write(expr('int?')), expr('bool'))) |
| .then([ |
| break_(), |
| ]), |
| relationalPattern('==', nullLiteral).then([ |
| checkReachable(true), |
| checkNotPromoted(x), |
| ]), |
| wildcard(expectInferredType: 'int').then([ |
| checkReachable(true), |
| checkNotPromoted(x), |
| ]), |
| ]), |
| ]); |
| }); |
| |
| test('Null pattern promotes matched pattern var', () { |
| h.run([ |
| ifCase( |
| expr('int?'), |
| relationalPattern('==', nullLiteral) |
| .or(wildcard(expectInferredType: 'int')), |
| []), |
| ]); |
| }); |
| |
| test('Null pattern can even match non-nullable types', () { |
| // Due to mixed mode unsoundness, attempting to match `null` to a |
| // non-nullable type can still succeed, so in order to avoid |
| // unsoundness escalation, it's important that the matching case is |
| // considered reachable. |
| h.run([ |
| ifCase(expr('int'), relationalPattern('==', nullLiteral), [ |
| checkReachable(true), |
| ], [ |
| checkReachable(true), |
| ]), |
| ]); |
| }); |
| |
| group('Demonstrated type:', () { |
| test('== value', () { |
| // The demonstrated type of a relational pattern using `==` is the |
| // matched value type. |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(Object?,)')), |
| ifCase( |
| x, |
| recordPattern( |
| [relationalPattern('==', expr('Object')).recordField()]), |
| [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| |
| test('== null', () { |
| // The demonstrated type of a relational pattern using `==` is the |
| // matched value type, even in the case of `== null`, because we |
| // don't promote to the `Null` type. |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(Object?,)')), |
| ifCase( |
| x, |
| recordPattern( |
| [relationalPattern('==', nullLiteral).recordField()]), |
| [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| }); |
| }); |
| |
| group('!=:', () { |
| test('Guaranteed mismatch due to Null type', () { |
| h.run([ |
| ifCase(expr('Null'), relationalPattern('!=', nullLiteral), [ |
| checkReachable(false), |
| ], [ |
| checkReachable(true), |
| ]) |
| ]); |
| }); |
| |
| test('In the general case, may or may not match', () { |
| h.run([ |
| ifCase(expr('Object?'), relationalPattern('!=', intLiteral(0)), [ |
| checkReachable(true), |
| ], [ |
| checkReachable(true), |
| ]), |
| ]); |
| }); |
| |
| test('Null pattern promotes unchanged scrutinee', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('int?')), |
| ifCase(x, relationalPattern('!=', nullLiteral), [ |
| checkReachable(true), |
| checkPromoted(x, 'int'), |
| ], [ |
| checkReachable(true), |
| checkNotPromoted(x), |
| ]) |
| ]); |
| }); |
| |
| test("Null pattern doesn't promote changed scrutinee", () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('int?')), |
| switch_(x, [ |
| wildcard() |
| .when(second(x.write(expr('int?')), expr('bool'))) |
| .then([ |
| break_(), |
| ]), |
| relationalPattern('!=', nullLiteral) |
| .and( |
| wildcard(expectInferredType: 'int')..errorId = 'WILDCARD') |
| .then([ |
| checkReachable(true), |
| checkNotPromoted(x), |
| ]), |
| ]), |
| ], expectedErrors: { |
| 'unnecessaryWildcardPattern(pattern: WILDCARD, ' |
| 'kind: logicalAndPatternOperand)' |
| }); |
| }); |
| |
| test('Null pattern promotes matched pattern var', () { |
| h.run([ |
| ifCase( |
| expr('int?'), |
| relationalPattern('!=', nullLiteral).and( |
| wildcard(expectInferredType: 'int')..errorId = 'WILDCARD'), |
| []), |
| ], expectedErrors: { |
| 'unnecessaryWildcardPattern(pattern: WILDCARD, ' |
| 'kind: logicalAndPatternOperand)' |
| }); |
| }); |
| |
| test('Null pattern can even match non-nullable types', () { |
| // Due to mixed mode unsoundness, attempting to match `null` to a |
| // non-nullable type can still succeed, so in order to avoid |
| // unsoundness escalation, it's important that the matching case is |
| // considered reachable. |
| h.run([ |
| ifCase(expr('int'), relationalPattern('!=', nullLiteral), [ |
| checkReachable(true), |
| ], [ |
| checkReachable(true), |
| ]), |
| ]); |
| }); |
| |
| group('Demonstrated type:', () { |
| test('!= value', () { |
| // The demonstrated type of a relational pattern using `!=` is |
| // usually the matched value type. |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(Object?,)')), |
| ifCase( |
| x, |
| recordPattern( |
| [relationalPattern('!=', expr('Object')).recordField()]), |
| [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| |
| test('!= null', () { |
| // The demonstrated type of the relational pattern `!= null` is the |
| // matched value type promoted to non-nullable. |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(Object?,)')), |
| ifCase( |
| x, |
| recordPattern( |
| [relationalPattern('!=', nullLiteral).recordField()]), |
| [ |
| checkPromoted(x, '(Object,)'), |
| ]), |
| ]); |
| }); |
| }); |
| }); |
| |
| group('other:', () { |
| test('Does not assume anything, even though == or != would', () { |
| // This is a bit of a contrived test case, since it exercises |
| // `null < null`. But such a thing is possible with extension |
| // methods. |
| h.addMember('Null', '<', 'bool Function(Object?)'); |
| h.run([ |
| ifCase(expr('Null'), relationalPattern('<', nullLiteral), [ |
| checkReachable(true), |
| ], [ |
| checkReachable(true), |
| ]) |
| ]); |
| }); |
| |
| test('Demonstrated type', () { |
| // The demonstrated type of a relational pattern using a |
| // non-equality operator is the matched value type. |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(int,)')), |
| ifCase( |
| x, |
| recordPattern( |
| [relationalPattern('>', intLiteral(0)).recordField()]), |
| [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| }); |
| }); |
| |
| group('Switch expression:', () { |
| test('guarded', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'int?'), |
| switchExpr(expr('Object'), [ |
| wildcard().when(x.notEq(nullLiteral)).thenExpr(second( |
| listLiteral(elementType: 'dynamic', [ |
| checkReachable(true), |
| checkPromoted(x, 'int'), |
| ]), |
| expr('String'))), |
| wildcard().thenExpr(second( |
| listLiteral(elementType: 'dynamic', [ |
| checkReachable(true), |
| checkNotPromoted(x), |
| ]), |
| expr('String'))), |
| ]), |
| ]); |
| }); |
| |
| group('guard promotes later cases:', () { |
| test('when pattern fully covers the scrutinee type', () { |
| // `case _ when x == null:` promotes `x` to non-null in later cases, |
| // because the implicit type of `_` fully covers the scrutinee type. |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'int?'), |
| switchExpr(expr('Object?'), [ |
| wildcard().when(x.eq(nullLiteral)).thenExpr(intLiteral(0)), |
| wildcard() |
| .thenExpr(second(checkPromoted(x, 'int'), intLiteral(1))), |
| ]), |
| ]); |
| }); |
| |
| test('when pattern does not fully cover the scrutinee type', () { |
| // `case String _ when x == null:` does not promote `y` to non-null in |
| // later cases, because the type `String` does not fully cover the |
| // scrutinee type. |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'int?'), |
| switchExpr(expr('Object?'), [ |
| wildcard(type: 'String') |
| .when(x.eq(nullLiteral)) |
| .thenExpr(intLiteral(0)), |
| wildcard().thenExpr(second( |
| listLiteral(elementType: 'dynamic', [ |
| checkNotPromoted(x), |
| ]), |
| intLiteral(1))), |
| ]), |
| ]); |
| }); |
| }); |
| |
| test('promotes scrutinee', () { |
| var x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(x, type: 'num'), |
| switchExpr(x, [ |
| y.pattern(type: 'int').thenExpr(second( |
| listLiteral(elementType: 'dynamic', [ |
| checkReachable(true), |
| checkPromoted(x, 'int'), |
| ]), |
| expr('String'))), |
| wildcard().thenExpr(second( |
| listLiteral(elementType: 'dynamic', [ |
| checkReachable(true), |
| checkNotPromoted(x), |
| ]), |
| expr('String'))), |
| ]), |
| ]); |
| }); |
| |
| test('reassigned scrutinee var no longer promotes', () { |
| var x = Var('x'); |
| // Note that the second `wildcard(type: 'int')` doesn't promote `x` |
| // because it's been reassigned. But it does still promote the |
| // scrutinee in the RHS of the `&&`. |
| h.run([ |
| declare(x, initializer: expr('Object')), |
| switchExpr(x, [ |
| wildcard(type: 'int') |
| .and(wildcard(expectInferredType: 'int')..errorId = 'WILDCARD1') |
| .thenExpr(second(checkPromoted(x, 'int'), intLiteral(0))), |
| wildcard() |
| .when(second(x.write(expr('Object')), expr('bool'))) |
| .thenExpr(intLiteral(1)), |
| wildcard(type: 'int') |
| .and(wildcard(expectInferredType: 'int')..errorId = 'WILDCARD2') |
| .thenExpr(second(checkNotPromoted(x), intLiteral(2))), |
| wildcard().thenExpr(intLiteral(3)), |
| ]), |
| ], expectedErrors: { |
| 'unnecessaryWildcardPattern(pattern: WILDCARD1, ' |
| 'kind: logicalAndPatternOperand)', |
| 'unnecessaryWildcardPattern(pattern: WILDCARD2, ' |
| 'kind: logicalAndPatternOperand)', |
| }); |
| }); |
| |
| test( |
| 'cached scrutinee retains promoted type even if scrutinee var ' |
| 'reassigned', () { |
| var x = Var('x'); |
| var y = Var('y'); |
| // `x` is promoted at the time the scrutinee is cached. Therefore, even |
| // though `case _ where f(x = ...)` de-promotes `x`, the promoted type |
| // is still used for type inference in the later `case var y`. |
| h.run([ |
| declare(x, initializer: expr('Object')), |
| x.as_('int'), |
| checkPromoted(x, 'int'), |
| switchExpr(x, [ |
| wildcard() |
| .when(second(x.write(expr('Object')), expr('bool'))) |
| .thenExpr(intLiteral(0)), |
| y |
| .pattern(expectInferredType: 'int') |
| .thenExpr(second(checkNotPromoted(x), intLiteral(1))), |
| ]), |
| ]); |
| }); |
| |
| test('no cases', () { |
| h.run([ |
| switchExpr(expr('A'), []), |
| checkReachable(false), |
| ]); |
| }); |
| |
| test('error type does not make following cases unreachable', () { |
| // We don't know the correct type, so recover by expecting that the |
| // following cases still will be useful once the error is fixed. |
| h.run([ |
| switchExpr(expr('num'), [ |
| wildcard(type: 'error') |
| .thenExpr(second(checkReachable(true), intLiteral(0))), |
| wildcard().thenExpr(second(checkReachable(true), intLiteral(1))), |
| ]), |
| ]); |
| }); |
| }); |
| |
| group('Switch statement:', () { |
| test('guarded', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'int?'), |
| switch_(expr('Object'), [ |
| switchStatementMember([ |
| wildcard().when(x.notEq(nullLiteral)) |
| ], [ |
| checkReachable(true), |
| checkPromoted(x, 'int'), |
| ]), |
| switchStatementMember([ |
| default_ |
| ], [ |
| checkReachable(true), |
| checkNotPromoted(x), |
| ]), |
| ]), |
| ]); |
| }); |
| |
| group('guard promotes later cases:', () { |
| test('when pattern fully covers the scrutinee type', () { |
| // `case _ when x == null:` promotes `x` to non-null in later cases, |
| // because the implicit type of `_` fully covers the scrutinee type. |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'int?'), |
| switch_(expr('Object?'), [ |
| wildcard().when(x.eq(nullLiteral)).then([break_()]), |
| wildcard().then([ |
| checkPromoted(x, 'int'), |
| ]), |
| ]), |
| ]); |
| }); |
| |
| test('when pattern does not fully cover the scrutinee type', () { |
| // `case String _ when x == null:` does not promote `x` to non-null in |
| // later cases, because the type `String` does not fully cover the |
| // scrutinee type. |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'int?'), |
| switch_(expr('Object?'), [ |
| wildcard(type: 'String').when(x.eq(nullLiteral)).then([break_()]), |
| wildcard().then([ |
| checkNotPromoted(x), |
| ]), |
| ]), |
| ]); |
| }); |
| }); |
| |
| test('promotes scrutinee', () { |
| var x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(x, type: 'num'), |
| switch_(x, [ |
| switchStatementMember([ |
| y.pattern(type: 'int') |
| ], [ |
| checkReachable(true), |
| checkPromoted(x, 'int'), |
| ]), |
| switchStatementMember([ |
| default_ |
| ], [ |
| checkReachable(true), |
| checkNotPromoted(x), |
| ]), |
| ]), |
| ]); |
| }); |
| |
| test('implicit break', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'Object'), |
| switch_(expr('Object'), [ |
| switchStatementMember([ |
| wildcard(type: 'int') |
| ], [ |
| x.as_('int'), |
| ]), |
| switchStatementMember([default_], [return_()]) |
| ]), |
| checkReachable(true), |
| checkPromoted(x, 'int'), |
| ]); |
| }); |
| |
| group('exhaustiveness:', () { |
| test('exhaustive', () { |
| h.addExhaustiveness('E', true); |
| h.run([ |
| switch_(expr('E'), [ |
| switchStatementMember([ |
| expr('E').pattern, |
| ], [ |
| return_(), |
| ]) |
| ]), |
| checkReachable(false), |
| ]); |
| }); |
| |
| test('non-exhaustive', () { |
| h.run([ |
| switch_(expr('int'), [ |
| switchStatementMember([ |
| intLiteral(0).pattern, |
| ], [ |
| return_(), |
| ]) |
| ]), |
| checkReachable(true), |
| ]); |
| }); |
| }); |
| |
| group('pre-patterns exhaustiveness:', () { |
| test('exhaustive', () { |
| h.disablePatterns(); |
| h.run([ |
| switch_( |
| expr('E'), |
| [ |
| switchStatementMember([ |
| expr('E').pattern, |
| ], [ |
| return_(), |
| ]) |
| ], |
| isLegacyExhaustive: true), |
| checkReachable(false), |
| ]); |
| }); |
| |
| test('non-exhaustive', () { |
| h.disablePatterns(); |
| h.run([ |
| switch_( |
| expr('E'), |
| [ |
| switchStatementMember([ |
| expr('E').pattern, |
| ], [ |
| return_(), |
| ]) |
| ], |
| isLegacyExhaustive: false), |
| checkReachable(true), |
| ]); |
| }); |
| }); |
| |
| test('empty exhaustive', () { |
| // This can happen if a class is marked `sealed` but has no subclasses. |
| // Note that exhaustiveness checking of "always exhaustive" types is |
| // deferred until a later analysis stage (so that it can take constant |
| // evaluation into account), so flow analysis simply assumes that the |
| // switch is exhaustive without checking, and sets the |
| // `requiresExhaustivenessValidation` flag to let the client know that |
| // exhaustiveness checking must be performed later. Had this been a |
| // real compilation (and not just a unit test), exhaustiveness checking |
| // would later confirm that the class `C` has no subclasses, or report |
| // a compile-time error. |
| h.addExhaustiveness('C', true); |
| h.run([ |
| switch_(expr('C'), [], expectRequiresExhaustivenessValidation: true), |
| checkReachable(false), |
| ]); |
| }); |
| |
| group('Nested:', () { |
| test('scrutinee type', () { |
| // Verify that the inner switch's matched value type doesn't bleed out |
| // to the next case in the outer switch. |
| h.run([ |
| switch_(expr('int'), [ |
| wildcard(expectInferredType: 'int').when(expr('bool')).then([ |
| switch_(expr('String'), [ |
| wildcard(expectInferredType: 'String').then([]), |
| ]), |
| ]), |
| wildcard(expectInferredType: 'int').then([]), |
| ]), |
| ]); |
| }); |
| |
| test('scrutinee reference', () { |
| // Verify that the inner switch's scrutinee reference is properly |
| // distinguished from the outer switch's scrutinee reference. |
| var x = Var('x'); |
| var y = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('Object')), |
| declare(y, initializer: expr('Object')), |
| switch_(x, [ |
| wildcard(type: 'num').then([ |
| checkPromoted(x, 'num'), |
| checkNotPromoted(y), |
| switch_(y, [ |
| wildcard(type: 'int').then([ |
| checkPromoted(x, 'num'), |
| checkPromoted(y, 'int'), |
| ]), |
| default_.then([ |
| return_(), |
| ]), |
| ]), |
| checkPromoted(x, 'num'), |
| checkPromoted(y, 'int'), |
| ]), |
| wildcard(type: 'String').then([ |
| checkPromoted(x, 'String'), |
| checkNotPromoted(y), |
| ]), |
| ]), |
| ]); |
| }); |
| }); |
| |
| test('reassigned scrutinee var no longer promotes', () { |
| var x = Var('x'); |
| // Note that the second `wildcard(type: 'int')` doesn't promote `x` |
| // because it's been reassigned. But it does still promote the |
| // scrutinee in the RHS of the `&&`. |
| h.run([ |
| declare(x, initializer: expr('Object')), |
| switch_(x, [ |
| wildcard(type: 'int') |
| .and(wildcard(expectInferredType: 'int')..errorId = 'WILDCARD1') |
| .then([ |
| checkPromoted(x, 'int'), |
| ]), |
| wildcard() |
| .when(second(x.write(expr('Object')), expr('bool'))) |
| .then([ |
| break_(), |
| ]), |
| wildcard(type: 'int') |
| .and(wildcard(expectInferredType: 'int')..errorId = 'WILDCARD2') |
| .then([ |
| checkNotPromoted(x), |
| ]) |
| ]), |
| ], expectedErrors: { |
| 'unnecessaryWildcardPattern(pattern: WILDCARD1, ' |
| 'kind: logicalAndPatternOperand)', |
| 'unnecessaryWildcardPattern(pattern: WILDCARD2, ' |
| 'kind: logicalAndPatternOperand)' |
| }); |
| }); |
| |
| test( |
| 'cached scrutinee retains promoted type even if scrutinee var ' |
| 'reassigned', () { |
| var x = Var('x'); |
| var y = Var('y'); |
| // `x` is promoted at the time the scrutinee is cached. Therefore, even |
| // though `case _ where f(x = ...)` de-promotes `x`, the promoted type |
| // is still used for type inference in the later `case var y`. |
| h.run([ |
| declare(x, initializer: expr('Object')), |
| x.as_('int'), |
| checkPromoted(x, 'int'), |
| switch_(x, [ |
| wildcard() |
| .when(second(x.write(expr('Object')), expr('bool'))) |
| .then([break_()]), |
| y.pattern(expectInferredType: 'int').then([ |
| checkNotPromoted(x), |
| ]), |
| ]), |
| ]); |
| }); |
| |
| test('synthetic break inserted even in unreachable cases', () { |
| // In this example, the second case is unreachable, so technically it |
| // doesn't matter whether it ends in a synthetic break. However, to |
| // avoid confusion on the part of the CFE and back-end developers, we go |
| // ahead and put in the synthetic break anyhow. |
| h.run([ |
| switch_(expr('Object'), [ |
| wildcard().then([ |
| intLiteral(0), |
| ]), |
| wildcard().then([ |
| intLiteral(1), |
| ]), |
| ]).checkIR('switch(expr(Object), ' |
| 'case(heads(head(wildcardPattern(matchedType: Object), true, ' |
| 'variables()), variables()), block(stmt(0), synthetic-break())), ' |
| 'case(heads(head(wildcardPattern(matchedType: Object), true, ' |
| 'variables()), variables()), ' |
| 'block(stmt(1), synthetic-break())))'), |
| ]); |
| }); |
| |
| test('error type does not make following cases unreachable', () { |
| // We don't know the correct type, so recover by expecting that the |
| // following cases still will be useful once the error is fixed. |
| h.run([ |
| switch_(expr('num'), [ |
| wildcard(type: 'error').then([ |
| checkReachable(true), |
| ]), |
| wildcard().then([ |
| checkReachable(true), |
| ]), |
| ]), |
| ]); |
| }); |
| |
| group('Joins promotions of scrutinee:', () { |
| test('First case more promoted', () { |
| var x = Var('x'); |
| // ` case num() && int(): case num():` retains promotion to `num` |
| h.run([ |
| declare(x, initializer: expr('Object')), |
| switch_(x, [ |
| switchStatementMember([ |
| objectPattern(requiredType: 'num', fields: []) |
| .and(objectPattern(requiredType: 'int', fields: [])), |
| objectPattern(requiredType: 'num', fields: []) |
| ], [ |
| checkPromoted(x, 'num'), |
| ]) |
| ]), |
| ]); |
| }); |
| |
| test('Second case more promoted', () { |
| var x = Var('x'); |
| // `case num(): case num() && int():` retains promotion to `num` |
| h.run([ |
| declare(x, initializer: expr('Object')), |
| switch_(x, [ |
| switchStatementMember([ |
| objectPattern(requiredType: 'num', fields: []), |
| objectPattern(requiredType: 'num', fields: []) |
| .and(objectPattern(requiredType: 'int', fields: [])) |
| ], [ |
| checkPromoted(x, 'num'), |
| ]) |
| ]), |
| ]); |
| }); |
| }); |
| |
| group('Joins explicitly declared variables:', () { |
| test('First var promoted', () { |
| var x1 = Var('x', identity: 'x1'); |
| var x2 = Var('x', identity: 'x2'); |
| var x = PatternVariableJoin('x', expectedComponents: [x1, x2]); |
| h.run([ |
| switch_(expr('(int, int?)'), [ |
| switchStatementMember([ |
| recordPattern([ |
| intLiteral(0).pattern.recordField(), |
| x1.pattern(type: 'int?').nullCheck.recordField() |
| ]), |
| recordPattern([ |
| intLiteral(1).pattern.recordField(), |
| x2.pattern(type: 'int?').recordField() |
| ]) |
| ], [ |
| checkNotPromoted(x), |
| ]) |
| ]), |
| ]); |
| }); |
| |
| test('Second var promoted', () { |
| var x1 = Var('x', identity: 'x1'); |
| var x2 = Var('x', identity: 'x2'); |
| var x = PatternVariableJoin('x', expectedComponents: [x1, x2]); |
| h.run([ |
| switch_(expr('(int, int?)'), [ |
| switchStatementMember([ |
| recordPattern([ |
| intLiteral(0).pattern.recordField(), |
| x1.pattern(type: 'int?').recordField() |
| ]), |
| recordPattern([ |
| intLiteral(1).pattern.recordField(), |
| x2.pattern(type: 'int?').nullCheck.recordField() |
| ]) |
| ], [ |
| checkNotPromoted(x), |
| ]) |
| ]), |
| ]); |
| }); |
| |
| test('Both vars promoted', () { |
| var x1 = Var('x', identity: 'x1'); |
| var x2 = Var('x', identity: 'x2'); |
| var x = PatternVariableJoin('x', expectedComponents: [x1, x2]); |
| h.run([ |
| switch_(expr('int?'), [ |
| switchStatementMember([ |
| x1.pattern(type: 'int?').nullCheck, |
| x2.pattern(type: 'int?').nullCheck |
| ], [ |
| checkPromoted(x, 'int'), |
| ]) |
| ]), |
| ]); |
| }); |
| |
| test('Promoted via when clause', () { |
| // Equivalent Dart code: |
| // switch (... as (int, int?)) { |
| // case (0, int? x?): |
| // case (1, int? x) where x != null: |
| // x; // Should be promoted to non-null |
| // } |
| var x1 = Var('x', identity: 'x1'); |
| var x2 = Var('x', identity: 'x2'); |
| var x = PatternVariableJoin('x', expectedComponents: [x1, x2]); |
| h.run([ |
| switch_(expr('(int, int?)'), [ |
| switchStatementMember([ |
| recordPattern([ |
| intLiteral(0).pattern.recordField(), |
| x1.pattern(type: 'int?').nullCheck.recordField() |
| ]), |
| recordPattern([ |
| intLiteral(1).pattern.recordField(), |
| x2.pattern(type: 'int?').recordField() |
| ]).when(x2.notEq(nullLiteral)), |
| ], [ |
| checkPromoted(x, 'int'), |
| ]) |
| ]), |
| ]); |
| }); |
| |
| test('Complex example', () { |
| // This is based on the code sample from |
| // https://github.com/dart-lang/sdk/issues/51644, except that the type |
| // of the scrutinee has been changed from `dynamic` to `Object?`. |
| var a1 = Var('a', identity: 'a1'); |
| var a2 = Var('a', identity: 'a2'); |
| var a3 = Var('a', identity: 'a3'); |
| var a = PatternVariableJoin('a', expectedComponents: [a1, a2, a3]); |
| h.run([ |
| switch_(expr('Object?'), [ |
| switchStatementMember([ |
| a1.pattern(type: 'String?').nullCheck.when(a1.is_('Never')), |
| a2.pattern(type: 'String?').when(a2.notEq(nullLiteral)), |
| a3 |
| .pattern(type: 'String?') |
| .nullAssert |
| .when(a3.eq(intLiteral(1))), |
| ], [ |
| checkPromoted(a, 'String'), |
| ]), |
| ]), |
| ]); |
| }); |
| |
| test('Join variable is promotable', () { |
| var x1 = Var('x', identity: 'x1'); |
| var x2 = Var('x', identity: 'x2'); |
| var x = PatternVariableJoin('x', expectedComponents: [x1, x2]); |
| h.run([ |
| switch_(expr('int?'), [ |
| switchStatementMember([ |
| x1.pattern(type: 'int?').nullCheck, |
| x2.pattern(type: 'int?') |
| ], [ |
| checkNotPromoted(x), |
| x.nonNullAssert, |
| checkPromoted(x, 'int'), |
| ]), |
| ]) |
| ]); |
| }); |
| }); |
| |
| group( |
| "Sets join variable assigned even if variable doesn't appear in " |
| "every case", () { |
| test('Variable in first case only', () { |
| var x1 = Var('x', identity: 'x1'); |
| var x = PatternVariableJoin('x', expectedComponents: [x1]); |
| // `x` is considered assigned inside the case body (even though it's |
| // not actually assigned by both patterns) because this avoids |
| // redundant errors. |
| h.run([ |
| switch_(expr('num?'), [ |
| switchStatementMember([ |
| x1.pattern().nullCheck, |
| wildcard() |
| ], [ |
| checkAssigned(x, true), |
| // Also verify that the join variable is promotable |
| checkNotPromoted(x), |
| x.as_('int'), |
| checkPromoted(x, 'int'), |
| ]) |
| ]), |
| ]); |
| }); |
| |
| test('Variable in second case only', () { |
| var x1 = Var('x', identity: 'x1'); |
| var x = PatternVariableJoin('x', expectedComponents: [x1]); |
| // `x` is considered assigned inside the case body (even though it's |
| // not actually assigned by both patterns) because this avoids |
| // redundant errors. |
| h.run([ |
| switch_(expr('int?'), [ |
| switchStatementMember([ |
| wildcard().nullCheck, |
| x1.pattern() |
| ], [ |
| checkAssigned(x, true), |
| // Also verify that the join variable is promotable |
| checkNotPromoted(x), |
| x.nonNullAssert, |
| checkPromoted(x, 'int'), |
| ]) |
| ]), |
| ]); |
| }); |
| }); |
| |
| group('Trivial exhaustiveness:', () { |
| // Although flow analysis doesn't attempt to do full exhaustiveness |
| // checking on switch statements, it understands that if any single case |
| // fully covers the matched value type, the switch statement is |
| // exhaustive. (Such a switch is called "trivially exhaustive"). |
| // |
| // Note that we don't test all possible patterns, because the flow |
| // analysis logic for detecting trivial exhaustiveness builds on the |
| // logic for tracking the "unmatched" state, which is tested elsewhere. |
| test('exhaustive', () { |
| h.run([ |
| switch_(expr('Object'), [ |
| wildcard().then([ |
| return_(), |
| ]), |
| ]), |
| checkReachable(false), |
| ]); |
| }); |
| |
| test('exhaustive but a reachable switch case completes', () { |
| // In this case, even though the switch is trivially exhaustive, the |
| // code after the switch is reachable because one of the reachable |
| // switch cases completes normally. |
| h.run([ |
| switch_(expr('Object'), [ |
| wildcard(type: 'int').then([ |
| checkReachable(true), |
| ]), |
| wildcard().then([ |
| return_(), |
| ]), |
| ]), |
| checkReachable(true), |
| ]); |
| }); |
| |
| test('exhaustive but an unreachable switch case completes', () { |
| // In this case, even though the `int` case completes normally, that |
| // case is unreachable, so the code after the switch is unreachable. |
| h.run([ |
| switch_(expr('Object'), [ |
| wildcard().then([ |
| return_(), |
| ]), |
| wildcard(type: 'int').then([ |
| checkReachable(false), |
| ]), |
| ]), |
| checkReachable(false), |
| ]); |
| }); |
| |
| test('exhaustive but a reachable switch case breaks', () { |
| // In this case, even though the switch is trivially exhaustive, the |
| // code after the switch is reachable because one of the reachable |
| // switch cases ends in a break. |
| h.run([ |
| switch_(expr('Object'), [ |
| wildcard(type: 'int').then([ |
| checkReachable(true), |
| break_(), |
| ]), |
| wildcard().then([ |
| return_(), |
| ]), |
| ]), |
| checkReachable(true), |
| ]); |
| }); |
| |
| test('exhaustive but an unreachable switch case breaks', () { |
| // In this case, even though the `int` case breaks, that case is |
| // unreachable, so the code after the switch is unreachable. |
| h.run([ |
| switch_(expr('Object'), [ |
| wildcard().then([ |
| return_(), |
| ]), |
| wildcard(type: 'int').then([ |
| checkReachable(false), |
| break_(), |
| ]), |
| ]), |
| checkReachable(false), |
| ]); |
| }); |
| |
| test('not exhaustive', () { |
| h.run([ |
| switch_(expr('Object'), [ |
| wildcard(type: 'int').then([ |
| return_(), |
| ]), |
| ]), |
| checkReachable(true), |
| ]); |
| }); |
| }); |
| }); |
| |
| group('Variable pattern:', () { |
| group('covers matched type:', () { |
| test('without promotion candidate', () { |
| // In `if(<some int> case num x) ...`, the `else` branch should be |
| // unreachable because the type `num` fully covers the type `int`. |
| var x = Var('x'); |
| h.run([ |
| ifCase(expr('int'), x.pattern(type: 'num'), [ |
| checkReachable(true), |
| ], [ |
| checkReachable(false), |
| ]), |
| ]); |
| }); |
| |
| test('with promotion candidate', () { |
| // In `if(x case num y) ...`, the `else` branch should be unreachable |
| // because the type `num` fully covers the type `int`. |
| var x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(x, type: 'int'), |
| ifCase(x, y.pattern(type: 'num'), [ |
| checkReachable(true), |
| checkNotPromoted(x), |
| ], [ |
| checkReachable(false), |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| |
| test('matched type is extension type', () { |
| h.addSuperInterfaces('E', (_) => [Type('Object?')]); |
| h.addExtensionTypeErasure('E', 'int'); |
| var x = Var('x'); |
| h.run([ |
| ifCase(expr('E'), x.pattern(type: 'int'), [ |
| checkReachable(true), |
| ], [ |
| checkReachable(false), |
| ]), |
| ]); |
| }); |
| |
| test('known type is extension type', () { |
| h.addSuperInterfaces('E', (_) => [Type('Object?')]); |
| h.addExtensionTypeErasure('E', 'int'); |
| var x = Var('x'); |
| h.run([ |
| ifCase(expr('int'), x.pattern(type: 'E'), [ |
| checkReachable(true), |
| ], [ |
| checkReachable(false), |
| ]), |
| ]); |
| }); |
| }); |
| |
| group("doesn't cover matched type:", () { |
| test('without promotion candidate', () { |
| // In `if(<some num> case int x) ...`, the `else` branch should be |
| // reachable because the type `int` doesn't fully cover the type |
| // `num`. |
| var x = Var('x'); |
| h.run([ |
| ifCase(expr('num'), x.pattern(type: 'int'), [ |
| checkReachable(true), |
| ], [ |
| checkReachable(true), |
| ]), |
| ]); |
| }); |
| |
| group('with promotion candidate:', () { |
| test('without factor', () { |
| var x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(x, type: 'num'), |
| ifCase(x, y.pattern(type: 'int'), [ |
| checkReachable(true), |
| checkPromoted(x, 'int'), |
| ], [ |
| checkReachable(true), |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| |
| test('with factor', () { |
| var x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(x, type: 'int?'), |
| ifCase(x, y.pattern(type: 'Null'), [ |
| checkReachable(true), |
| checkPromoted(x, 'Null'), |
| ], [ |
| checkReachable(true), |
| checkPromoted(x, 'int'), |
| ]), |
| ]); |
| }); |
| }); |
| }); |
| |
| test("Subpattern doesn't promote scrutinee", () { |
| var x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(x, initializer: expr('Object')), |
| ifCase( |
| x, |
| objectPattern( |
| requiredType: 'num', |
| fields: [y.pattern(type: 'int').recordField('sign')]), |
| [ |
| checkPromoted(x, 'num'), |
| // TODO(paulberry): should promote `x.sign` to `int`. |
| ]), |
| ]); |
| }); |
| |
| test("Doesn't demote", () { |
| var x = Var('x'); |
| var y = Var('y'); |
| var z = Var('z'); |
| h.run([ |
| declare(x, initializer: expr('Object?')), |
| ifCase( |
| x, |
| wildcard() |
| .as_('int') |
| .and(y.pattern(type: 'num')) |
| .and(z.pattern(expectInferredType: 'int')), |
| [ |
| checkPromoted(x, 'int'), |
| ]), |
| ]); |
| }); |
| |
| test('Promotes to non-nullable if matched type is non-nullable', () { |
| // When the matched value type is non-nullable, and the variable's |
| // declared type is nullable, a successful match promotes the variable. |
| // This allows a case pattern of the form `T? x?` to promote `x` to |
| // non-nullable `T`. |
| var x = Var('x'); |
| h.run([ |
| ifCase(expr('Object'), x.pattern(type: 'int?'), [ |
| checkPromoted(x, 'int'), |
| ]), |
| ]); |
| }); |
| |
| test('Does not promote to non-nullable if matched type is `Null`', () { |
| // Since `Null` is handled specially by `TypeOperations.classifyType`, |
| // make sure that we don't accidentally promote the variable to |
| // non-nullable when the matched value type is `Null`. |
| var x = Var('x'); |
| h.run([ |
| ifCase(expr('Null'), x.pattern(type: 'int?'), [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| |
| group('Demonstrated type:', () { |
| test('Subtype of matched value type', () { |
| var x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(x, initializer: expr('(num,)')), |
| ifCase(x, recordPattern([y.pattern(type: 'int').recordField()]), [ |
| checkPromoted(x, '(int,)'), |
| ]), |
| ]); |
| }); |
| |
| test('Supertype of matched value type', () { |
| var x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(x, initializer: expr('(num,)')), |
| ifCase( |
| x, recordPattern([y.pattern(type: 'Object').recordField()]), [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| |
| test('Unrelated to matched value type', () { |
| var x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(x, initializer: expr('(num,)')), |
| ifCase( |
| x, recordPattern([y.pattern(type: 'String').recordField()]), [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| }); |
| |
| test('Promotable property', () { |
| h.addMember('C', '_property', 'Object', promotable: true); |
| var c = Var('c'); |
| var x = Var('x'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| ifCase(c.property('_property'), x.pattern(type: 'int'), [ |
| checkPromoted(c.property('_property'), 'int'), |
| ]), |
| ]); |
| }); |
| |
| test('Promotable property, target changed', () { |
| h.addMember('C', '_property', 'Object', promotable: true); |
| var c = Var('c'); |
| var x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| switch_(c.property('_property'), [ |
| x.pattern(type: 'int').when(expr('bool')).then([ |
| checkPromoted(c.property('_property'), 'int'), |
| ]), |
| wildcard().when(second(c.write(expr('C')), expr('bool'))).then([]), |
| y.pattern(type: 'int').then([ |
| checkNotPromoted(c.property('_property')), |
| ]), |
| ]), |
| ]); |
| }); |
| |
| test('Non-promotable property', () { |
| h.addMember('C', '_property', 'Object', promotable: false); |
| var c = Var('c'); |
| var x = Var('x'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| ifCase(c.property('_property'), x.pattern(type: 'int'), [ |
| checkNotPromoted(c.property('_property')), |
| ]), |
| ]); |
| }); |
| }); |
| |
| group('Wildcard pattern:', () { |
| group('covers matched type:', () { |
| test('without promotion candidate', () { |
| // In `if(<some int> case num _) ...`, the `else` branch should be |
| // unreachable because the type `num` fully covers the type `int`. |
| h.run([ |
| ifCase(expr('int'), wildcard(type: 'num'), [ |
| checkReachable(true), |
| ], [ |
| checkReachable(false), |
| ]), |
| ]); |
| }); |
| |
| test('with promotion candidate', () { |
| // In `if(x case num _) ...`, the `else` branch should be unreachable |
| // because the type `num` fully covers the type `int`. |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'int'), |
| ifCase(x, wildcard(type: 'num'), [ |
| checkReachable(true), |
| checkNotPromoted(x), |
| ], [ |
| checkReachable(false), |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| }); |
| |
| group("doesn't cover matched type:", () { |
| test('without promotion candidate', () { |
| // In `if(<some num> case int _) ...`, the `else` branch should be |
| // reachable because the type `int` doesn't fully cover the type |
| // `num`. |
| h.run([ |
| ifCase(expr('num'), wildcard(type: 'int'), [ |
| checkReachable(true), |
| ], [ |
| checkReachable(true), |
| ]), |
| ]); |
| }); |
| |
| group('with promotion candidate:', () { |
| test('without factor', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'num'), |
| ifCase(x, wildcard(type: 'int'), [ |
| checkReachable(true), |
| checkPromoted(x, 'int'), |
| ], [ |
| checkReachable(true), |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| |
| test('with factor', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, type: 'int?'), |
| ifCase(x, wildcard(type: 'Null'), [ |
| checkReachable(true), |
| checkPromoted(x, 'Null'), |
| ], [ |
| checkReachable(true), |
| checkPromoted(x, 'int'), |
| ]), |
| ]); |
| }); |
| }); |
| }); |
| |
| test("Subpattern doesn't promote scrutinee", () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('Object')), |
| ifCase( |
| x, |
| objectPattern( |
| requiredType: 'num', |
| fields: [wildcard(type: 'int').recordField('sign')]), |
| [ |
| checkPromoted(x, 'num'), |
| // TODO(paulberry): should promote `x.sign` to `int`. |
| ]), |
| ]); |
| }); |
| |
| test("Doesn't demote", () { |
| var x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(x, initializer: expr('Object?')), |
| ifCase( |
| x, |
| wildcard() |
| .as_('int') |
| .and(wildcard(type: 'num')..errorId = 'WILDCARD') |
| .and(y.pattern(expectInferredType: 'int')), |
| [ |
| checkPromoted(x, 'int'), |
| ]), |
| ], expectedErrors: { |
| 'unnecessaryWildcardPattern(pattern: WILDCARD, ' |
| 'kind: logicalAndPatternOperand)', |
| }); |
| }); |
| |
| group('Demonstrated type:', () { |
| test('Subtype of matched value type', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(num,)')), |
| ifCase(x, recordPattern([wildcard(type: 'int').recordField()]), [ |
| checkPromoted(x, '(int,)'), |
| ]), |
| ]); |
| }); |
| |
| test('Supertype of matched value type', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(num,)')), |
| ifCase(x, recordPattern([wildcard(type: 'Object').recordField()]), [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| |
| test('Unrelated to matched value type', () { |
| var x = Var('x'); |
| h.run([ |
| declare(x, initializer: expr('(num,)')), |
| ifCase(x, recordPattern([wildcard(type: 'String').recordField()]), [ |
| checkNotPromoted(x), |
| ]), |
| ]); |
| }); |
| }); |
| |
| test('Promotable property', () { |
| h.addMember('C', '_property', 'Object', promotable: true); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| ifCase(c.property('_property'), wildcard(type: 'int'), [ |
| checkPromoted(c.property('_property'), 'int'), |
| ]), |
| ]); |
| }); |
| |
| test('Promotable property, target changed', () { |
| h.addMember('C', '_property', 'Object', promotable: true); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| switch_(c.property('_property'), [ |
| wildcard(type: 'int').when(expr('bool')).then([ |
| checkPromoted(c.property('_property'), 'int'), |
| ]), |
| wildcard().when(second(c.write(expr('C')), expr('bool'))).then([]), |
| wildcard(type: 'int').then([ |
| checkNotPromoted(c.property('_property')), |
| ]), |
| ]), |
| ]); |
| }); |
| |
| test('Non-promotable property', () { |
| h.addMember('C', '_property', 'Object', promotable: false); |
| var c = Var('c'); |
| h.run([ |
| declare(c, initializer: expr('C')), |
| ifCase(c.property('_property'), wildcard(type: 'int'), [ |
| checkNotPromoted(c.property('_property')), |
| ]), |
| ]); |
| }); |
| }); |
| |
| test('Pattern inside guard', () { |
| // Roughly equivalent Dart code: |
| // FutureOr<int> x = ...; |
| // FutureOr<String> y = ...; |
| // if (x case int _ when f(() { |
| // if (y case String _) { |
| // /* x promoted to `int` */ |
| // /* y promoted to `String` */ |
| // } else { |
| // /* x promoted to `int` */ |
| // /* y promoted to `Future<String>` */ |
| // } |
| // }, throw ...)) { |
| // /* unreachable (due to `throw`) */ |
| // } else { |
| // /* x promoted to `Future<int>` */ |
| // } |
| // For this to be analyzed correctly, flow analysis needs to avoid mixing |
| // up the "unmatched" state from the outer and inner pattern matches. |
| var x = Var('x'); |
| var y = Var('y'); |
| h.run([ |
| declare(x, initializer: expr('FutureOr<int>')), |
| declare(y, initializer: expr('FutureOr<String>')), |
| ifCase( |
| x, |
| wildcard(type: 'int').when(second( |
| localFunction([ |
| ifCase(y, wildcard(type: 'String'), [ |
| checkPromoted(x, 'int'), |
| checkPromoted(y, 'String'), |
| ], [ |
| checkPromoted(x, 'int'), |
| checkPromoted(y, 'Future<String>'), |
| ]), |
| ]), |
| throw_(expr('Object')))), |
| [ |
| checkReachable(false), |
| ], |
| [ |
| checkReachable(true), |
| checkPromoted(x, 'Future<int>'), |
| ]), |
| ]); |
| }); |
| |
| test('Error type does not trigger unnecessary wildcard warning', () { |
| h.run([ |
| ifCase(expr('num'), wildcard(type: 'int').and(wildcard(type: 'error')), |
| []), |
| ]); |
| }); |
| |
| group('Split points:', () { |
| test('Guarded', () { |
| // This test verifies that for a guarded pattern, the join of the two |
| // "unmatched" control flow paths corresponds to a split point at the |
| // beginning of the pattern. |
| var i = Var('i'); |
| h.run([ |
| declare(i, initializer: expr('int?')), |
| ifCase( |
| second(throw_(expr('Object')), expr('int')).checkType('int'), |
| objectPattern(requiredType: 'int', fields: []) |
| .when(i.eq(nullLiteral)), |
| [], |
| [ |
| // There is a join point here, joining the flow control paths |
| // where (a) the pattern `int()` failed to match and (b) the |
| // guard `i == null` was not satisfied. Since the scrutinee has |
| // type `int`, and the pattern is `int()`, the pattern is |
| // guaranteed to match, so path (a) is unreachable. Path (b) is |
| // also unreachable due to the fact that the scrutinee throws, |
| // but since the split point is the beginning of the pattern, |
| // path (b) is reachable from the split point. So the promotion |
| // implied by (b) is preserved after the join. |
| checkPromoted(i, 'int'), |
| // Note that due to the `throw` in the scrutinee, this code is |
| // unreachable. |
| checkReachable(false), |
| ]), |
| ]); |
| }); |
| |
| test('Logical-or', () { |
| // This test verifies that for a logical-or pattern, the join of the two |
| // "matched" control flow paths corresponds to a split point at the |
| // beginning of the top level pattern. |
| var x = Var('x'); |
| h.run([ |
| ifCase( |
| expr('(Null, Null, int?)'), |
| recordPattern([ |
| relationalPattern('!=', nullLiteral).recordField(), |
| wildcard().recordField(), |
| wildcard().recordField() |
| ]) |
| // At this point, control flow is unreachable due to the fact |
| // that the `!= null` pattern in the first field of the |
| // record pattern above can never match the type `Null`. |
| .and(recordPattern([ |
| wildcard().recordField(), |
| relationalPattern('!=', nullLiteral).recordField(), |
| wildcard().recordField() |
| ]) |
| // At this point, control flow is unreachable for a |
| // second reason: because the `!= null` pattern in the |
| // second field of the record pattern above can never |
| // match the type `Null`. |
| .or(recordPattern([ |
| wildcard().recordField(), |
| wildcard().recordField(), |
| wildcard().nullCheck.recordField() |
| ]) |
| // At this point, the third field of the scrutinee |
| // is promoted from `int?` to `int`, due to the |
| // null check pattern. |
| ) |
| // At this point, there is a control flow join between the |
| // two branches of the logical-or pattern. Since the split |
| // point corresponding to the control flow join is at the |
| // beginning of the top level pattern, both branches are |
| // considered unreachable, so neither is favored in the |
| // join, and therefore, the promotion from the second |
| // branch is lost. |
| ) |
| .and( |
| // The record pattern below matches `x` to the unpromoted |
| // type of the third field of the scrutinee, so we just |
| // have to verify that it has the expected type of `int?`. |
| recordPattern([ |
| wildcard().recordField(), |
| wildcard().recordField(), |
| x.pattern(expectInferredType: 'int?').recordField() |
| ])), |
| [ |
| // As a sanity check, confirm that the overall pattern |
| // can't ever match. |
| checkReachable(false), |
| ]), |
| ]); |
| }); |
| }); |
| }); |
| } |
| |
| /// Returns the appropriate matcher for expecting an assertion error to be |
| /// thrown or not, based on whether assertions are enabled. |
| Matcher get _asserts { |
| var matcher = throwsA(TypeMatcher<AssertionError>()); |
| bool assertionsEnabled = false; |
| assert(assertionsEnabled = true); |
| if (!assertionsEnabled) { |
| matcher = isNot(matcher); |
| } |
| return matcher; |
| } |
| |
| String _describeMatcher(Matcher matcher) { |
| var description = StringDescription(); |
| matcher.describe(description); |
| return description.toString(); |
| } |
| |
| Matcher _matchOfInterestSet(List<String> expectedTypes) { |
| return predicate( |
| (List<SharedTypeView<Type>> x) => unorderedEquals(expectedTypes) |
| .matches(x.map((t) => t.unwrapTypeView().type).toList(), {}), |
| 'interest set $expectedTypes'); |
| } |
| |
| Matcher _matchPromotionChain(List<String>? expectedTypes) { |
| if (expectedTypes == null) return isNull; |
| return predicate( |
| (List<SharedTypeView<Type>> x) => equals(expectedTypes) |
| .matches(x.map((t) => t.unwrapTypeView().type).toList(), {}), |
| 'promotion chain $expectedTypes'); |
| } |
| |
| Matcher _matchVariableModel( |
| {Object? chain, |
| Object? ofInterest, |
| Object? assigned, |
| Object? unassigned, |
| Object? writeCaptured}) { |
| chain ??= anything; |
| ofInterest ??= anything; |
| assigned ??= anything; |
| unassigned ??= anything; |
| writeCaptured ??= anything; |
| Matcher chainMatcher = |
| chain is List<String> ? _matchPromotionChain(chain) : wrapMatcher(chain); |
| Matcher ofInterestMatcher = ofInterest is List<String> |
| ? _matchOfInterestSet(ofInterest) |
| : wrapMatcher(ofInterest); |
| Matcher assignedMatcher = wrapMatcher(assigned); |
| Matcher unassignedMatcher = wrapMatcher(unassigned); |
| Matcher writeCapturedMatcher = wrapMatcher(writeCaptured); |
| return predicate((PromotionModel<SharedTypeView<Type>> model) { |
| if (!chainMatcher.matches(model.promotedTypes, {})) return false; |
| if (!ofInterestMatcher.matches(model.tested, {})) return false; |
| if (!assignedMatcher.matches(model.assigned, {})) return false; |
| if (!unassignedMatcher.matches(model.unassigned, {})) return false; |
| if (!writeCapturedMatcher.matches(model.writeCaptured, {})) return false; |
| return true; |
| }, |
| 'VariableModel(chain: ${_describeMatcher(chainMatcher)}, ' |
| 'ofInterest: ${_describeMatcher(ofInterestMatcher)}, ' |
| 'assigned: ${_describeMatcher(assignedMatcher)}, ' |
| 'unassigned: ${_describeMatcher(unassignedMatcher)}, ' |
| 'writeCaptured: ${_describeMatcher(writeCapturedMatcher)})'); |
| } |
| |
| class _MockNonPromotionReason extends NonPromotionReason { |
| @override |
| NonPromotionDocumentationLink get documentationLink => |
| fail('Unexpected call to documentationLink'); |
| |
| @override |
| String get shortName => fail('Unexpected call to shortName'); |
| |
| @override |
| R accept<R, Node extends Object, Variable extends Object, |
| Type extends Object>( |
| NonPromotionReasonVisitor<R, Node, Variable, Type> visitor) => |
| fail('Unexpected call to accept'); |
| } |
| |
| extension on FlowModel<SharedTypeView<Type>> { |
| FlowModel<SharedTypeView<Type>> _conservativeJoin(FlowAnalysisTestHarness h, |
| Iterable<Var> writtenVariables, Iterable<Var> capturedVariables) => |
| conservativeJoin(h, [ |
| for (Var v in writtenVariables) h.promotionKeyStore.keyForVariable(v) |
| ], [ |
| for (Var v in capturedVariables) h.promotionKeyStore.keyForVariable(v) |
| ]); |
| |
| FlowModel<SharedTypeView<Type>> _declare( |
| FlowAnalysisTestHarness h, Var variable, bool initialized) => |
| this.declare( |
| h, h.promotionKeyStore.keyForVariable(variable), initialized); |
| |
| PromotionModel<SharedTypeView<Type>> _infoFor( |
| FlowAnalysisTestHarness h, Var variable) => |
| infoFor(h, h.promotionKeyStore.keyForVariable(variable), |
| ssaNode: new SsaNode(null)); |
| |
| FlowModel<SharedTypeView<Type>> _setInfo(FlowAnalysisTestHarness h, |
| Map<int, PromotionModel<SharedTypeView<Type>>> newInfo) { |
| var result = this; |
| for (var core.MapEntry(:key, :value) in newInfo.entries) { |
| if (result.promotionInfo?.get(h, key) != value) { |
| result = result.updatePromotionInfo(h, key, value); |
| } |
| } |
| return result; |
| } |
| |
| ExpressionInfo<SharedTypeView<Type>> _tryMarkNonNullable( |
| FlowAnalysisTestHarness h, Var variable) => |
| tryMarkNonNullable(h, _varRefWithType(h, variable)); |
| |
| ExpressionInfo<SharedTypeView<Type>> _tryPromoteForTypeCheck( |
| FlowAnalysisTestHarness h, Var variable, String type) => |
| tryPromoteForTypeCheck( |
| h, _varRefWithType(h, variable), SharedTypeView(Type(type))); |
| |
| int _varRef(FlowAnalysisTestHarness h, Var variable) => |
| h.promotionKeyStore.keyForVariable(variable); |
| |
| TrivialVariableReference<SharedTypeView<Type>> _varRefWithType( |
| FlowAnalysisTestHarness h, Var variable) => |
| new TrivialVariableReference<SharedTypeView<Type>>( |
| promotionKey: _varRef(h, variable), |
| after: this, |
| type: promotionInfo |
| ?.get(h, h.promotionKeyStore.keyForVariable(variable)) |
| ?.promotedTypes |
| ?.last ?? |
| SharedTypeView(variable.type), |
| isThisOrSuper: false, |
| ssaNode: SsaNode(null)); |
| |
| FlowModel<SharedTypeView<Type>> _write( |
| FlowAnalysisTestHarness h, |
| NonPromotionReason? nonPromotionReason, |
| Var variable, |
| SharedTypeView<Type> writtenType, |
| SsaNode<SharedTypeView<Type>> newSsaNode) => |
| write(h, nonPromotionReason, h.promotionKeyStore.keyForVariable(variable), |
| writtenType, newSsaNode, h.typeOperations, |
| unpromotedType: SharedTypeView(variable.type)); |
| } |
| |
| extension on PromotionInfo<SharedTypeView<Type>>? { |
| Map<int, PromotionModel<SharedTypeView<Type>>> unwrap( |
| FlowAnalysisTestHarness h) => |
| { |
| for (var FlowLinkDiffEntry(:int key, right: second!) |
| in h.reader.diff(null, this).entries) |
| key: second.model |
| }; |
| } |