From 9f0e5229fa1cf0769da9d84896670a2e93e229ce Mon Sep 17 00:00:00 2001 From: Paul Berry Date: Mon, 2 Jun 2025 13:57:11 -0700 Subject: [PATCH] [flow analysis] Fix layering of type promotions in try/finally. A tricky part of the implementation of flow analysis is the handling of try/finally statements. Although promotions are tracked separately in the `try` and `finally` blocks, promotions from both blocks need to be merged together at the conclusion of the finally block. This creates an ambiguity, because each type in a promotion chain is required to be a subtype of the previous, and hence multiple promotions of the same variable are inherently ordered. The ambiguity is: when the promotions from the `try` and `finally` block are merged, which promotions should be applied first? In discussion with the language team, we've decided that the promotions from the `try` block should be applied first, because that matches the order of code execution. This change makes the behavior of flow analysis more uniform, which should make it easier to reason about and maintain. In practice, the difference in behavior is quite subtle, and I don't expect users to notice. However, to be on the safe side, the change in behavior is conditioned on the `sound-flow-analysis` flag, so it will only take effect when the user deliberately upgrades to language version 3.9, and it will not affect already-published packages. A test in google3 showed that no internal code would be broken by force-enabling this change. Fixes https://github.com/dart-lang/language/issues/4382. Change-Id: I0e9f6db808a964e0b4325d3020654a9f2be273a2 Bug: https://github.com/dart-lang/language/issues/4382 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/432001 Reviewed-by: Konstantin Shcheglov Commit-Queue: Paul Berry --- .../lib/src/flow_analysis/flow_analysis.dart | 22 +- .../flow_analysis/flow_analysis_mini_ast.dart | 4 + .../flow_analysis/flow_analysis_test.dart | 355 +++++++++++++++++- .../proper_subtypes_test.dart | 44 ++- .../try_finally_layering_disabled_test.dart | 184 +++++++++ .../try_finally_layering_test.dart | 184 +++++++++ 6 files changed, 753 insertions(+), 40 deletions(-) create mode 100644 tests/language/sound_flow_analysis/try_finally_layering_disabled_test.dart create mode 100644 tests/language/sound_flow_analysis/try_finally_layering_test.dart diff --git a/pkg/_fe_analyzer_shared/lib/src/flow_analysis/flow_analysis.dart b/pkg/_fe_analyzer_shared/lib/src/flow_analysis/flow_analysis.dart index 002764fddf1..ec912f17ea7 100644 --- a/pkg/_fe_analyzer_shared/lib/src/flow_analysis/flow_analysis.dart +++ b/pkg/_fe_analyzer_shared/lib/src/flow_analysis/flow_analysis.dart @@ -2668,11 +2668,18 @@ class FlowModel { // // In all of these cases, the correct thing to do is to keep all // promotions that were done in both the `try` and `finally` blocks. - newPromotedTypes = PromotionModel.rebasePromotedTypes( - helper, - thisModel.promotedTypes, - afterFinallyModel.promotedTypes, - ); + newPromotedTypes = + helper.typeAnalyzerOptions.soundFlowAnalysisEnabled + ? PromotionModel.rebasePromotedTypes( + helper, + afterFinallyModel.promotedTypes, + thisModel.promotedTypes, + ) + : PromotionModel.rebasePromotedTypes( + helper, + thisModel.promotedTypes, + afterFinallyModel.promotedTypes, + ); // And we can safely restore the SSA node from the end of the try block. newSsaNode = thisModel.ssaNode; if (newSsaNode != afterFinallyModel.ssaNode) { @@ -3382,6 +3389,9 @@ mixin FlowModelHelper { @visibleForTesting PromotionKeyStore get promotionKeyStore; + /// Language features enables affecting the behavior of flow analysis. + TypeAnalyzerOptions get typeAnalyzerOptions; + /// The [FlowAnalysisTypeOperations], used to access types and check /// subtyping. @visibleForTesting @@ -4988,7 +4998,7 @@ class _FlowAnalysisImpl< implements FlowAnalysis, _PropertyTargetHelper { - /// Language features enables affecting the behavior of flow analysis. + @override final TypeAnalyzerOptions typeAnalyzerOptions; /// The [FlowAnalysisOperations], used to access types, check subtyping, and diff --git a/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_mini_ast.dart b/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_mini_ast.dart index 16d875a3165..e550f28ff15 100644 --- a/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_mini_ast.dart +++ b/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_mini_ast.dart @@ -6,6 +6,7 @@ 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/type_inference/promotion_key_store.dart'; import 'package:_fe_analyzer_shared/src/type_inference/type_analysis_result.dart'; +import 'package:_fe_analyzer_shared/src/type_inference/type_analyzer.dart'; import 'package:_fe_analyzer_shared/src/types/shared_type.dart'; import '../mini_ast.dart'; @@ -38,6 +39,9 @@ class FlowAnalysisTestHarness extends Harness @override final SharedTypeView boolType = SharedTypeView(Type('bool')); + @override + TypeAnalyzerOptions get typeAnalyzerOptions => computeTypeAnalyzerOptions(); + @override FlowAnalysisOperations get typeOperations => typeAnalyzer.operations; diff --git a/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart b/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart index 2bbdca8c084..d308aca7a41 100644 --- a/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart +++ b/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart @@ -12200,14 +12200,13 @@ main() { x.as_('List'), checkPromoted(x, 'List'), ]), - // After the try/finally, the promotions in the try block are - // layered over the promotions in the finally block (see - // https://github.com/dart-lang/language/issues/4382), so the - // promotion to `List` layers over the promotion to - // `List`. But since the two types are mutual subtypes, the - // promotion to `List` is discarded, leaving only the - // promotion to `List`. - checkPromoted(x, 'List'), + // After the try/finally, the promotions in the finally block are + // layered over the promotions in the try block, so the + // promotion to `List` layers over the promotion to + // `List`. But since the two types are mutual subtypes, the + // promotion to `List` is discarded, leaving only the + // promotion to `List`. + checkPromoted(x, 'List'), ]); }); @@ -12224,14 +12223,13 @@ main() { x.property('_property').as_('List'), checkPromoted(x.property('_property'), 'List'), ]), - // After the try/finally, the promotions in the try block are - // layered over the promotions in the finally block (see - // https://github.com/dart-lang/language/issues/4382), so the - // promotion to `List` layers over the promotion to - // `List`. But since the two types are mutual subtypes, the - // promotion to `List` is discarded, leaving only the - // promotion to `List`. - checkPromoted(x.property('_property'), 'List'), + // After the try/finally, the promotions in the finally block are + // layered over the promotions in the try block, so the + // promotion to `List` layers over the promotion to + // `List`. But since the two types are mutual subtypes, the + // promotion to `List` is discarded, leaving only the + // promotion to `List`. + checkPromoted(x.property('_property'), 'List'), ]); }); @@ -12281,6 +12279,331 @@ main() { ]); }); }); + + group('Try/finally layering order:', () { + group('Local variables:', () { + test('When disabled, promotions in `finally` applied first', () { + h.disableSoundFlowAnalysis(); + var x = Var('x'); + var y = Var('y'); + h.run([ + declare(x, initializer: expr('Object')), + declare(y, initializer: expr('Object')), + if_( + expr('bool'), + [ + x.as_('num'), + y.as_('num'), + // The promotion chains for `x` and `y` are both `[num]`. + checkPromoted(x, 'num'), + checkPromoted(y, 'num'), + ], + [ + try_([ + x.as_('num'), + y.as_('int'), + checkPromoted(x, 'num'), + checkPromoted(y, 'int'), + ]).finally_([ + // Neither `x` nor `y` is promoted at this point, because in + // principle an exception could have occurred at any point in + // the `try` block. + checkNotPromoted(x), + checkNotPromoted(y), + x.as_('int'), + y.as_('num'), + checkPromoted(x, 'int'), + checkPromoted(y, 'num'), + ]), + // After the try/finally, both `x` and `y` are fully promoted to + // `int`. + checkPromoted(x, 'int'), + checkPromoted(y, 'int'), + // But since the promotions from the `try` block are layered + // over the promotions from the `finally` block, `x` has + // promotion chain `[int]`, whereas `y` has promotion chain + // `[num, int]`. Therefore, after the `if` and `else` control + // flow paths are joined... + ], + ), + // `x` is no longer promoted at all (since `[num]` and `[int]` have + // no types in common), whereas `y` is promoted to `num` (since + // `[num]` and `[num, int]` both contain the type `num`). + checkNotPromoted(x), + checkPromoted(y, 'num'), + ]); + }); + + test('When enabled, promotions in `try` applied first', () { + var x = Var('x'); + var y = Var('y'); + h.run([ + declare(x, initializer: expr('Object')), + declare(y, initializer: expr('Object')), + if_( + expr('bool'), + [ + x.as_('num'), + y.as_('num'), + // The promotion chains for `x` and `y` are both `[num]`. + checkPromoted(x, 'num'), + checkPromoted(y, 'num'), + ], + [ + try_([ + x.as_('num'), + y.as_('int'), + checkPromoted(x, 'num'), + checkPromoted(y, 'int'), + ]).finally_([ + // Neither `x` nor `y` is promoted at this point, because in + // principle an exception could have occurred at any point in + // the `try` block. + checkNotPromoted(x), + checkNotPromoted(y), + x.as_('int'), + y.as_('num'), + checkPromoted(x, 'int'), + checkPromoted(y, 'num'), + ]), + // After the try/finally, both `x` and `y` are fully promoted to + // `int`. + checkPromoted(x, 'int'), + checkPromoted(y, 'int'), + // But since the promotions from the `finally` block are layered + // over the promotions from the `try` block, `x` has + // promotion chain `[num, int]`, whereas `y` has promotion chain + // `[int]`. Therefore, after the `if` and `else` control flow + // paths are joined... + ], + ), + // `x` is promoted to `num` (since `[num]` and `[num, int]` both + // contain the type `num`), whereas `y` is no longer promoted at all + // (since `[num]` and `[int]` have no types in common). + checkPromoted(x, 'num'), + checkNotPromoted(y), + ]); + }); + }); + + group('Fields of unmodified local variables:', () { + test('When disabled, promotions in `finally` applied first', () { + h.disableSoundFlowAnalysis(); + h.addMember('C', '_f', 'Object', promotable: true); + var x = Var('x'); + var y = Var('y'); + h.run([ + declare(x, initializer: expr('C')), + declare(y, initializer: expr('C')), + if_( + expr('bool'), + [ + x.property('_f').as_('num'), + y.property('_f').as_('num'), + // The promotion chains for `x._f` and `y._f` are both `[num]`. + checkPromoted(x.property('_f'), 'num'), + checkPromoted(y.property('_f'), 'num'), + ], + [ + try_([ + x.property('_f').as_('num'), + y.property('_f').as_('int'), + checkPromoted(x.property('_f'), 'num'), + checkPromoted(y.property('_f'), 'int'), + ]).finally_([ + // Neither `x._f` nor `y._f` is promoted at this point, + // because in principle an exception could have occurred at + // any point in the `try` block. + checkNotPromoted(x.property('_f')), + checkNotPromoted(y.property('_f')), + x.property('_f').as_('int'), + y.property('_f').as_('num'), + checkPromoted(x.property('_f'), 'int'), + checkPromoted(y.property('_f'), 'num'), + ]), + // After the try/finally, both `x._f` and `y._f` are fully + // promoted to `int`. + checkPromoted(x.property('_f'), 'int'), + checkPromoted(y.property('_f'), 'int'), + // But since the promotions from the `try` block are layered + // over the promotions from the `finally` block, `x._f` has + // promotion chain `[int]`, whereas `y._f` has promotion chain + // `[num, int]`. Therefore, after the `if` and `else` control + // flow paths are joined... + ], + ), + // `x._f` is no longer promoted at all (since `[num]` and `[int]` + // have no types in common), whereas `y._f` is promoted to `num` + // (since `[num]` and `[num, int]` both contain the type `num`). + checkNotPromoted(x.property('_f')), + checkPromoted(y.property('_f'), 'num'), + ]); + }); + + test('When enabled, promotions in `try` applied first', () { + h.addMember('C', '_f', 'Object', promotable: true); + var x = Var('x'); + var y = Var('y'); + h.run([ + declare(x, initializer: expr('C')), + declare(y, initializer: expr('C')), + if_( + expr('bool'), + [ + x.property('_f').as_('num'), + y.property('_f').as_('num'), + // The promotion chains for `x._f` and `y._f` are both `[num]`. + checkPromoted(x.property('_f'), 'num'), + checkPromoted(y.property('_f'), 'num'), + ], + [ + try_([ + x.property('_f').as_('num'), + y.property('_f').as_('int'), + checkPromoted(x.property('_f'), 'num'), + checkPromoted(y.property('_f'), 'int'), + ]).finally_([ + // Neither `x._f` nor `y._f` is promoted at this point, + // because in principle an exception could have occurred at + // any point in the `try` block. + checkNotPromoted(x.property('_f')), + checkNotPromoted(y.property('_f')), + x.property('_f').as_('int'), + y.property('_f').as_('num'), + checkPromoted(x.property('_f'), 'int'), + checkPromoted(y.property('_f'), 'num'), + ]), + // After the try/finally, both `x._f` and `y._f` are fully + // promoted to `int`. + checkPromoted(x.property('_f'), 'int'), + checkPromoted(y.property('_f'), 'int'), + // But since the promotions from the `finally` block are layered + // over the promotions from the `try` block, `x._f` has + // promotion chain `[num, int]`, whereas `y._f` has promotion + // chain `[int]`. Therefore, after the `if` and `else` control + // flow paths are joined... + ], + ), + // `x._f` is promoted to `num` (since `[num]` and `[num, int]` both + // contain the type `num`), whereas `y._f` is no longer promoted at + // all (since `[num]` and `[int]` have no types in common). + checkPromoted(x.property('_f'), 'num'), + checkNotPromoted(y.property('_f')), + ]); + }); + }); + + group('Fields of local variables modified in try clause:', () { + test('When disabled, promotions in `try` applied first', () { + h.disableSoundFlowAnalysis(); + h.addMember('C', '_f', 'Object', promotable: true); + var x = Var('x'); + var y = Var('y'); + h.run([ + declare(x, initializer: expr('C')), + declare(y, initializer: expr('C')), + if_( + expr('bool'), + [ + x.property('_f').as_('num'), + y.property('_f').as_('num'), + // The promotion chains for `x._f` and `y._f` are both `[num]`. + checkPromoted(x.property('_f'), 'num'), + checkPromoted(y.property('_f'), 'num'), + ], + [ + try_([ + x.write(expr('C')), + y.write(expr('C')), + x.property('_f').as_('num'), + y.property('_f').as_('int'), + checkPromoted(x.property('_f'), 'num'), + checkPromoted(y.property('_f'), 'int'), + ]).finally_([ + // Neither `x._f` nor `y._f` is promoted at this point, + // because in principle an exception could have occurred at + // any point in the `try` block. + checkNotPromoted(x.property('_f')), + checkNotPromoted(y.property('_f')), + x.property('_f').as_('int'), + y.property('_f').as_('num'), + checkPromoted(x.property('_f'), 'int'), + checkPromoted(y.property('_f'), 'num'), + ]), + // After the try/finally, both `x._f` and `y._f` are fully + // promoted to `int`. + checkPromoted(x.property('_f'), 'int'), + checkPromoted(y.property('_f'), 'int'), + // But since the promotions from the `finally` block are layered + // over the promotions from the `try` block, `x._f` has + // promotion chain `[num, int]`, whereas `y._f` has promotion + // chain `[int]`. Therefore, after the `if` and `else` control + // flow paths are joined... + ], + ), + // `x._f` is promoted to `num` (since `[num]` and `[num, int]` both + // contain the type `num`), whereas `y._f` is no longer promoted at + // all (since `[num]` and `[int]` have no types in common). + checkPromoted(x.property('_f'), 'num'), + checkNotPromoted(y.property('_f')), + ]); + }); + + test('When enabled, promotions in `try` applied first', () { + h.addMember('C', '_f', 'Object', promotable: true); + var x = Var('x'); + var y = Var('y'); + h.run([ + declare(x, initializer: expr('C')), + declare(y, initializer: expr('C')), + if_( + expr('bool'), + [ + x.property('_f').as_('num'), + y.property('_f').as_('num'), + // The promotion chains for `x._f` and `y._f` are both `[num]`. + checkPromoted(x.property('_f'), 'num'), + checkPromoted(y.property('_f'), 'num'), + ], + [ + try_([ + x.write(expr('C')), + y.write(expr('C')), + x.property('_f').as_('num'), + y.property('_f').as_('int'), + checkPromoted(x.property('_f'), 'num'), + checkPromoted(y.property('_f'), 'int'), + ]).finally_([ + // Neither `x._f` nor `y._f` is promoted at this point, + // because in principle an exception could have occurred at + // any point in the `try` block. + checkNotPromoted(x.property('_f')), + checkNotPromoted(y.property('_f')), + x.property('_f').as_('int'), + y.property('_f').as_('num'), + checkPromoted(x.property('_f'), 'int'), + checkPromoted(y.property('_f'), 'num'), + ]), + // After the try/finally, both `x._f` and `y._f` are fully + // promoted to `int`. + checkPromoted(x.property('_f'), 'int'), + checkPromoted(y.property('_f'), 'int'), + // But since the promotions from the `finally` block are layered + // over the promotions from the `try` block, `x._f` has + // promotion chain `[num, int]`, whereas `y._f` has promotion + // chain `[int]`. Therefore, after the `if` and `else` control + // flow paths are joined... + ], + ), + // `x._f` is promoted to `num` (since `[num]` and `[num, int]` both + // contain the type `num`), whereas `y._f` is no longer promoted at + // all (since `[num]` and `[int]` have no types in common). + checkPromoted(x.property('_f'), 'num'), + checkNotPromoted(y.property('_f')), + ]); + }); + }); + }); }); group('Demotion and type of interest promotion:', () { diff --git a/tests/language/sound_flow_analysis/proper_subtypes_test.dart b/tests/language/sound_flow_analysis/proper_subtypes_test.dart index a8fe9a7d539..d2a80a09639 100644 --- a/tests/language/sound_flow_analysis/proper_subtypes_test.dart +++ b/tests/language/sound_flow_analysis/proper_subtypes_test.dart @@ -168,15 +168,18 @@ testFinallyClauseVariable(Object? x) { // Verify that the type really is `List` and not `List`. x.first.abs(); } - // After the try/finally, the promotions in the try block are layered over the - // promotions in the finally block (see - // https://github.com/dart-lang/language/issues/4382), so the promotion to - // `List` layers over the promotion to `List`. But since the - // two types are mutual subtypes, the promotion to `List` is - // discarded, leaving only the promotion to `List`. - x.expectStaticType>>(); - // Verify that the type really is `List` and not `List`. - x.first.abs(); + // After the try/finally, the promotions in the finally block are layered over + // the promotions in the try block, so the promotion to `List` layers + // over the promotion to `List`. But since the two types are mutual + // subtypes, the promotion to `List` is discarded, leaving only the + // promotion to `List`. + x.expectStaticType>>(); + // Verify that the type really is `List` and not `List`. + { + var z = x.first; + z!; + [z].expectStaticType>>(); + } } // When promotions from a `try` block and `finally` block are combined, a @@ -204,15 +207,20 @@ testFinallyClausePropertyOfUnmodifiedVariable(C x) { // Verify that the type really is `List` and not `List`. x._f.first.abs(); } - // After the try/finally, the promotions in the try block are layered over the - // promotions in the finally block (see - // https://github.com/dart-lang/language/issues/4382), so the promotion to - // `List` layers over the promotion to `List`. But since the - // two types are mutual subtypes, the promotion to `List` is - // discarded, leaving only the promotion to `List`. - x._f.expectStaticType>>(); - // Verify that the type really is `List` and not `List`. - x._f.first.abs(); + // After the try/finally, the promotions in the finally block are layered over + // the promotions in the try block, so the promotion to `List` layers + // over the promotion to `List`. But since the two types are mutual + // subtypes, the promotion to `List` is discarded, leaving only the + // promotion to `List`. + x._f.expectStaticType>>(); + // Verify that the type really is `List` and not `List`. + // (This works because applying `!` to a variable of type `Object?` promotes + // it, but applying `!` to a variable of type `dynamic` doesn't promote it.) + { + var z = x._f.first; + z!; + [z].expectStaticType>>(); + } } // When promotions from a `try` block and `finally` block are combined, a diff --git a/tests/language/sound_flow_analysis/try_finally_layering_disabled_test.dart b/tests/language/sound_flow_analysis/try_finally_layering_disabled_test.dart new file mode 100644 index 00000000000..3df09eba77a --- /dev/null +++ b/tests/language/sound_flow_analysis/try_finally_layering_disabled_test.dart @@ -0,0 +1,184 @@ +// Copyright (c) 2025, 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. + +// Tests how flow analysis layers promotions from `try` and `finally` blocks +// when `sound-flow-analysis` is disabled. + +// @dart = 3.8 + +import '../static_type_helper.dart'; + +class C { + final Object _f; + C(this._f); +} + +// For local variables that are not assigned in the `try` block, promotions in +// the `try` block are layered over promotions in the `finally` block. +void testUnassignedLocal(bool b, Object x, Object y) { + if (b) { + x as num; + y as num; + // The promotion chains for `x` and `y` are both `[num]`. + x.expectStaticType>(); + y.expectStaticType>(); + } else { + try { + x as num; + y as int; + x.expectStaticType>(); + y.expectStaticType>(); + } finally { + // Neither `x` nor `y` is promoted at this point, because in principle an + // exception could have occurred at any point in the `try` block. + x.expectStaticType>(); + y.expectStaticType>(); + x as int; + y as num; + } + // After the try/finally, both `x` and `y` are fully promoted to `int`. + x.expectStaticType>(); + y.expectStaticType>(); + // But since the promotions from the `try` block are layered over the + // promotions from the `finally` block, `x` has promotion chain `[int]`, + // whereas `y` has promotion chain `[num, int]`. Therefore, after the `if` + // and `else` control flow paths are joined... + } + // `x` is no longer promoted at all (since `[num]` and `[int]` have no types + // in common), whereas `y` is promoted to `num` (since `[num]` and `[num, + // int]` both contain the type `num`). + x.expectStaticType>(); + y.expectStaticType>(); +} + +// For local variables that are assigned in the `try` block, promotions in the +// `try` block are layered over promotions in the `finally` block. +void testAssignedLocal(bool b, Object x, Object y) { + if (b) { + x as num; + y as num; + // The promotion chains for `x` and `y` are both `[num]`. + x.expectStaticType>(); + y.expectStaticType>(); + } else { + try { + (x, y) = (y, x); + x as num; + y as int; + x.expectStaticType>(); + y.expectStaticType>(); + } finally { + // Neither `x` nor `y` is promoted at this point, because in principle an + // exception could have occurred at any point in the `try` block. + x.expectStaticType>(); + y.expectStaticType>(); + x as int; + y as num; + } + // After the try/finally, both `x` and `y` are fully promoted to `int`. + x.expectStaticType>(); + y.expectStaticType>(); + // But since the promotions from the `try` block are layered over the + // promotions from the `finally` block, `x` has promotion chain `[int]`, + // whereas `y` has promotion chain `[num, int]`. Therefore, after the `if` + // and `else` control flow paths are joined... + } + // `x` is no longer promoted at all (since `[num]` and `[int]` have no types + // in common), whereas `y` is promoted to `num` (since `[num]` and `[num, + // int]` both contain the type `num`). + x.expectStaticType>(); + y.expectStaticType>(); +} + +// For fields of local variables that are not assigned in the `try` block, +// promotions in the `try` block are layered over promotions in the `finally` +// block. +void testUnassignedField(bool b, C x, C y) { + if (b) { + x._f as num; + y._f as num; + // The promotion chains for `x._f` and `y._f` are both `[num]`. + x._f.expectStaticType>(); + y._f.expectStaticType>(); + } else { + try { + x._f as num; + y._f as int; + x._f.expectStaticType>(); + y._f.expectStaticType>(); + } finally { + // Neither `x._f` nor `y._f` is promoted at this point, because in + // principle an exception could have occurred at any point in the `try` + // block. + x._f.expectStaticType>(); + y._f.expectStaticType>(); + x._f as int; + y._f as num; + } + // After the try/finally, both `x._f` and `y._f` are fully promoted to + // `int`. + x._f.expectStaticType>(); + y._f.expectStaticType>(); + // But since the promotions from the `try` block are layered over the + // promotions from the `finally` block, `x._f` has promotion chain `[int]`, + // whereas `y._f` has promotion chain `[num, int]`. Therefore, after the + // `if` and `else` control flow paths are joined... + } + // `x._f` is no longer promoted at all (since `[num]` and `[int]` have no + // types in common), whereas `y._f` is promoted to `num` (since `[num]` and + // `[num, int]` both contain the type `num`). + x._f.expectStaticType>(); + y._f.expectStaticType>(); +} + +// For fields of local variables that are assigned in the `try` block, +// promotions in the `finally` block are layered over promotions in the `try` +// block. +void testAssignedField(bool b, C x, C y) { + if (b) { + x._f as num; + y._f as num; + // The promotion chains for `x._f` and `y._f` are both `[num]`. + x._f.expectStaticType>(); + y._f.expectStaticType>(); + } else { + try { + (x, y) = (y, x); + x._f as num; + y._f as int; + x._f.expectStaticType>(); + y._f.expectStaticType>(); + } finally { + // Neither `x._f` nor `y._f` is promoted at this point, because in + // principle an exception could have occurred at any point in the `try` + // block. + x._f.expectStaticType>(); + y._f.expectStaticType>(); + x._f as int; + y._f as num; + } + // After the try/finally, both `x._f` and `y._f` are fully promoted to + // `int`. + x._f.expectStaticType>(); + y._f.expectStaticType>(); + // But since the promotions from the `finally` block are layered over the + // promotions from the `try` block, `x._f` has promotion chain `[num, int]`, + // whereas `y._f` has promotion chain `[int]`. Therefore, after the `if` and + // `else` control flow paths are joined... + } + // `x._f` is promoted to `num` (since `[num]` and `[num, int]` both contain + // the type `num`), whereas `y._f` is no longer promoted at all (since `[num]` + // and `[int]` have no types in common). + x._f.expectStaticType>(); + y._f.expectStaticType>(); +} + +main() { + for (var b in [false, true]) { + testUnassignedLocal(b, 0, 0); + testAssignedLocal(b, 0, 0); + testUnassignedField(b, C(0), C(0)); + testAssignedField(b, C(0), C(0)); + } +} diff --git a/tests/language/sound_flow_analysis/try_finally_layering_test.dart b/tests/language/sound_flow_analysis/try_finally_layering_test.dart new file mode 100644 index 00000000000..526fbb5ca4d --- /dev/null +++ b/tests/language/sound_flow_analysis/try_finally_layering_test.dart @@ -0,0 +1,184 @@ +// Copyright (c) 2025, 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. + +// Tests how flow analysis layers promotions from `try` and `finally` blocks +// when `sound-flow-analysis` is enabled. + +// SharedOptions=--enable-experiment=sound-flow-analysis + +import '../static_type_helper.dart'; + +class C { + final Object _f; + C(this._f); +} + +// For local variables that are not assigned in the `try` block, promotions in +// the `finally` block are layered over promotions in the `try` block. +void testUnassignedLocal(bool b, Object x, Object y) { + if (b) { + x as num; + y as num; + // The promotion chains for `x` and `y` are both `[num]`. + x.expectStaticType>(); + y.expectStaticType>(); + } else { + try { + x as num; + y as int; + x.expectStaticType>(); + y.expectStaticType>(); + } finally { + // Neither `x` nor `y` is promoted at this point, because in principle an + // exception could have occurred at any point in the `try` block. + x.expectStaticType>(); + y.expectStaticType>(); + x as int; + y as num; + } + // After the try/finally, both `x` and `y` are fully promoted to `int`. + x.expectStaticType>(); + y.expectStaticType>(); + // But since the promotions from the `finally` block are layered over the + // promotions from the `try` block, `x` has promotion chain `[num, int]`, + // whereas `y` has promotion chain `[int]`. Therefore, after the `if` and + // `else` control flow paths are joined... + } + // `x` is promoted to `num` (since `[num]` and `[num, int]` both contain the + // type `num`), whereas `y` is no longer promoted at all (since `[num]` and + // `[int]` have no types in common). + x.expectStaticType>(); + y.expectStaticType>(); +} + +// For local variables that are assigned in the `try` block, promotions in the +// `finally` block are layered over promotions in the `try` block. +void testAssignedLocal(bool b, Object x, Object y) { + if (b) { + x as num; + y as num; + // The promotion chains for `x` and `y` are both `[num]`. + x.expectStaticType>(); + y.expectStaticType>(); + } else { + try { + (x, y) = (y, x); + x as num; + y as int; + x.expectStaticType>(); + y.expectStaticType>(); + } finally { + // Neither `x` nor `y` is promoted at this point, because in principle an + // exception could have occurred at any point in the `try` block. + x.expectStaticType>(); + y.expectStaticType>(); + x as int; + y as num; + } + // After the try/finally, both `x` and `y` are fully promoted to `int`. + x.expectStaticType>(); + y.expectStaticType>(); + // But since the promotions from the `finally` block are layered over the + // promotions from the `try` block, `x` has promotion chain `[num, int]`, + // whereas `y` has promotion chain `[int]`. Therefore, after the `if` and + // `else` control flow paths are joined... + } + // `x` is promoted to `num` (since `[num]` and `[num, int]` both contain the + // type `num`), whereas `y` is no longer promoted at all (since `[num]` and + // `[int]` have no types in common). + x.expectStaticType>(); + y.expectStaticType>(); +} + +// For fields of local variables that are not assigned in the `try` block, +// promotions in the `finally` block are layered over promotions in the `try` +// block. +void testUnassignedField(bool b, C x, C y) { + if (b) { + x._f as num; + y._f as num; + // The promotion chains for `x._f` and `y._f` are both `[num]`. + x._f.expectStaticType>(); + y._f.expectStaticType>(); + } else { + try { + x._f as num; + y._f as int; + x._f.expectStaticType>(); + y._f.expectStaticType>(); + } finally { + // Neither `x._f` nor `y._f` is promoted at this point, because in + // principle an exception could have occurred at any point in the `try` + // block. + x._f.expectStaticType>(); + y._f.expectStaticType>(); + x._f as int; + y._f as num; + } + // After the try/finally, both `x._f` and `y._f` are fully promoted to + // `int`. + x._f.expectStaticType>(); + y._f.expectStaticType>(); + // But since the promotions from the `finally` block are layered over the + // promotions from the `try` block, `x._f` has promotion chain `[num, int]`, + // whereas `y._f` has promotion chain `[int]`. Therefore, after the `if` and + // `else` control flow paths are joined... + } + // `x._f` is promoted to `num` (since `[num]` and `[num, int]` both contain + // the type `num`), whereas `y._f` is no longer promoted at all (since `[num]` + // and `[int]` have no types in common). + x._f.expectStaticType>(); + y._f.expectStaticType>(); +} + +// For fields of local variables that are assigned in the `try` block, +// promotions in the `finally` block are layered over promotions in the `try` +// block. +void testAssignedField(bool b, C x, C y) { + if (b) { + x._f as num; + y._f as num; + // The promotion chains for `x._f` and `y._f` are both `[num]`. + x._f.expectStaticType>(); + y._f.expectStaticType>(); + } else { + try { + (x, y) = (y, x); + x._f as num; + y._f as int; + x._f.expectStaticType>(); + y._f.expectStaticType>(); + } finally { + // Neither `x._f` nor `y._f` is promoted at this point, because in + // principle an exception could have occurred at any point in the `try` + // block. + x._f.expectStaticType>(); + y._f.expectStaticType>(); + x._f as int; + y._f as num; + } + // After the try/finally, both `x._f` and `y._f` are fully promoted to + // `int`. + x._f.expectStaticType>(); + y._f.expectStaticType>(); + // But since the promotions from the `finally` block are layered over the + // promotions from the `try` block, `x._f` has promotion chain `[num, int]`, + // whereas `y._f` has promotion chain `[int]`. Therefore, after the `if` and + // `else` control flow paths are joined... + } + // `x._f` is promoted to `num` (since `[num]` and `[num, int]` both contain + // the type `num`), whereas `y._f` is no longer promoted at all (since `[num]` + // and `[int]` have no types in common). + x._f.expectStaticType>(); + y._f.expectStaticType>(); +} + +main() { + for (var b in [false, true]) { + testUnassignedLocal(b, 0, 0); + testAssignedLocal(b, 0, 0); + testUnassignedField(b, C(0), C(0)); + testAssignedField(b, C(0), C(0)); + } +}