Skip to content

Commit 7436b97

Browse files
stereotype441Commit Queue
authored andcommitted
[flow analysis] Allow non-cascaded field accesses to participate in field promotion.
The way this is accomplished is that in `_FlowAnalysisImpl.nullAwareAccess_rightBegin`, any expression reference associated with the target of the null-aware access is restored, and the corresponding SSA node is associated with the guard variable (if any). These changes ensure that if the null-aware access is a property get, the subsequent call to `propertyGet` will pick up the appropriate SSA node, so it will be able to locate the promotion key for the property. This functionality is only enabled when the language feature `sound-flow-analysis` is enabled. To prevent test regressions, a few related changes need to be made at the same time: - `_FlowAnalysisImpl.nullAwareAccess_end` is changed so that it clears any expression info or expression reference that was associated with the null-aware access expression. This prevents flow analysis information from being erroneously propagated out of a null-aware expression, which would have led to assertion failures when analyzing null-aware expressions inside of conditional expressions. This wasn't previously a problem because the expression reference used to be consumed by `_FlowAnalysisImpl.nullAwareAccess_rightBegin`, preventing further expression references and expression infos from being recorded further along in the null-aware access. - The test framework in `mini_ast.dart` is fixed so that `!` is considered to participate in null shorting. This was a bug in the test framework that wasn't previously caught because it happened not to produce any test failures. - The analyzer's method `PostfixExpressionResolver._resolveNullCheck` is changed so that it calls `nonNullAssert_end` before terminating null-aware access. Previously, the order was swapped, causing `nullAwareAccess_end` to be called before `nonNullAssert_end` when analyzing expressions like `a?.b!`. This used to be benign, but now that non-cascaded field accesses participate in field promotion, flow analysis needs the methods to be called in the correct order. Fixes dart-lang/language#4344. Bug: dart-lang/language#4344 Change-Id: I523be1b4be1af3f68654a745187a546728c878fe Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/427820 Commit-Queue: Paul Berry <[email protected]> Reviewed-by: Konstantin Shcheglov <[email protected]>
1 parent 6d62d9a commit 7436b97

File tree

7 files changed

+214
-8
lines changed

7 files changed

+214
-8
lines changed

pkg/_fe_analyzer_shared/lib/src/flow_analysis/flow_analysis.dart

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5926,6 +5926,11 @@ class _FlowAnalysisImpl<
59265926
_NullAwareAccessContext<Type> context =
59275927
_stack.removeLast() as _NullAwareAccessContext<Type>;
59285928
_current = _join(_current, context._previous).unsplit();
5929+
// If any expression info or expression reference was stored for the
5930+
// null-aware expression, it was only valid in the case where the target
5931+
// expression was not null. So it needs to be cleared now.
5932+
_expressionInfo = null;
5933+
_expressionReference = null;
59295934
}
59305935

59315936
@override
@@ -5955,6 +5960,15 @@ class _FlowAnalysisImpl<
59555960
break;
59565961
}
59575962
_stack.add(new _NullAwareAccessContext<Type>(shortcutControlPath));
5963+
SsaNode<Type>? targetSsaNode;
5964+
if (typeAnalyzerOptions.soundFlowAnalysisEnabled) {
5965+
// Store back the target reference so that it can be used for field
5966+
// promotion.
5967+
if (target != null && targetReference != null) {
5968+
_storeExpressionReference(target, targetReference);
5969+
targetSsaNode = targetReference.ssaNode;
5970+
}
5971+
}
59585972
if (guardVariable != null) {
59595973
// Promote the guard variable as well.
59605974
int promotionKey = promotionKeyStore.keyForVariable(guardVariable);
@@ -5967,7 +5981,7 @@ class _FlowAnalysisImpl<
59675981
tested: const [],
59685982
assigned: true,
59695983
unassigned: false,
5970-
ssaNode: new SsaNode(null),
5984+
ssaNode: targetSsaNode ?? new SsaNode(null),
59715985
),
59725986
);
59735987
}

pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11885,6 +11885,109 @@ main() {
1188511885
]);
1188611886
});
1188711887
});
11888+
11889+
group('Null aware field access:', () {
11890+
group('Non-cascaded:', () {
11891+
test('When disabled, does not see previous promotions', () {
11892+
h.disableSoundFlowAnalysis();
11893+
h.addMember('A', '_i', 'int?', promotable: true);
11894+
var a = Var('a');
11895+
h.run([
11896+
declare(a, initializer: expr('A')),
11897+
a.property('_i').nonNullAssert,
11898+
// `a._i` is promoted now.
11899+
a.property('_i').checkType('int'),
11900+
// But `a?._i` is not.
11901+
a.property('_i', isNullAware: true).checkType('int?'),
11902+
]);
11903+
});
11904+
11905+
test('When enabled, sees previous promotions', () {
11906+
h.addMember('A', '_i', 'int?', promotable: true);
11907+
var a = Var('a');
11908+
h.run([
11909+
declare(a, initializer: expr('A')),
11910+
a.property('_i').nonNullAssert,
11911+
// `a._i` is promoted now.
11912+
a.property('_i').checkType('int'),
11913+
// And so is `a?._i`.
11914+
a.property('_i', isNullAware: true).checkType('int'),
11915+
]);
11916+
});
11917+
11918+
test('When disabled, cannot promote', () {
11919+
h.disableSoundFlowAnalysis();
11920+
h.addMember('A', '_i', 'int?', promotable: true);
11921+
var a = Var('a');
11922+
h.run([
11923+
declare(a, initializer: expr('A')),
11924+
a.property('_i', isNullAware: true).nonNullAssert,
11925+
// `a._i` is not promoted.
11926+
a.property('_i').checkType('int?'),
11927+
// But had the field access not been null aware, it would have been
11928+
// promoted.
11929+
a.property('_i').nonNullAssert,
11930+
a.property('_i').checkType('int'),
11931+
]);
11932+
});
11933+
11934+
test('When enabled, can promote', () {
11935+
h.addMember('A', '_i', 'int?', promotable: true);
11936+
var a = Var('a');
11937+
h.run([
11938+
declare(a, initializer: expr('A')),
11939+
a.property('_i').checkType('int?'),
11940+
a.property('_i', isNullAware: true).nonNullAssert,
11941+
// `a._i` is promoted.
11942+
a.property('_i').checkType('int'),
11943+
]);
11944+
});
11945+
11946+
test('In conditional expression', () {
11947+
h.addMember('A', '_i', 'int?', promotable: true);
11948+
var a = Var('a');
11949+
h.run([
11950+
declare(a, initializer: expr('A')),
11951+
expr(
11952+
'bool',
11953+
).conditional(nullLiteral, a.property('_i', isNullAware: true)),
11954+
]);
11955+
});
11956+
});
11957+
11958+
group('Cascaded:', () {
11959+
test('When disabled, sees previous promotions', () {
11960+
h.disableSoundFlowAnalysis();
11961+
h.addMember('A', '_i', 'int?', promotable: true);
11962+
var a = Var('a');
11963+
h.run([
11964+
declare(a, initializer: expr('A')),
11965+
a.property('_i').nonNullAssert,
11966+
// `a._i` is promoted now.
11967+
a.property('_i').checkType('int'),
11968+
// And `a?.._i` is promoted.
11969+
a.cascade(isNullAware: true, [
11970+
(placeholder) => placeholder.property('_i').checkType('int'),
11971+
]),
11972+
]);
11973+
});
11974+
11975+
test('When enabled, sees previous promotions', () {
11976+
h.addMember('A', '_i', 'int?', promotable: true);
11977+
var a = Var('a');
11978+
h.run([
11979+
declare(a, initializer: expr('A')),
11980+
a.property('_i').nonNullAssert,
11981+
// `a._i` is promoted now.
11982+
a.property('_i').checkType('int'),
11983+
// And `a?.._i` is promoted.
11984+
a.cascade(isNullAware: true, [
11985+
(placeholder) => placeholder.property('_i').checkType('int'),
11986+
]),
11987+
]);
11988+
});
11989+
});
11990+
});
1188811991
});
1188911992
}
1189011993

pkg/_fe_analyzer_shared/test/mini_ast.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6520,7 +6520,11 @@ class _MiniAstTypeAnalyzer
65206520
Expression node,
65216521
Expression expression,
65226522
) {
6523-
var type = analyzeExpression(expression, operations.unknownType);
6523+
var type = analyzeExpression(
6524+
expression,
6525+
operations.unknownType,
6526+
continueNullShorting: true,
6527+
);
65246528
flow.nonNullAssert_end(expression);
65256529
return new ExpressionTypeAnalysisResult(
65266530
type: flow.operations.promoteToNonNull(type),

pkg/analyzer/lib/src/dart/resolver/postfix_expression_resolver.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ class PostfixExpressionResolver {
235235
var type = _typeSystem.promoteToNonNull(operandType);
236236
node.recordStaticType(type, resolver: _resolver);
237237

238-
_resolver.nullShortingTermination(node);
239238
_resolver.flowAnalysis.flow?.nonNullAssert_end(operand);
239+
_resolver.nullShortingTermination(node);
240240
}
241241
}

pkg/front_end/lib/src/type_inference/inference_visitor.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9637,6 +9637,9 @@ class InferenceVisitorImpl extends InferenceVisitorBase
96379637
if (nonNullableType != variable.type) {
96389638
promotedType = nonNullableType;
96399639
}
9640+
// It's still necessary to inform flow analysis about the read so that it
9641+
// can track field promotions.
9642+
flowAnalysis.variableRead(node, variable);
96409643
} else if (!variable.isLocalFunction) {
96419644
// Don't promote local functions.
96429645
promotedType =

tests/language/inference_update_2/cascaded_field_promotion_unnecessary_null_aware_error_test.dart

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,10 @@ void fieldsPromotableWithinCascade(C c) {
5353
.._field!.expectStaticType<Exactly<Object>>()
5454
.._field.expectStaticType<Exactly<Object>>();
5555

56-
// After the cascade, the promotion is not retained, because of the implicit
57-
// control flow join implied by the `?..`. (In principle it would be sound to
58-
// preserve the promotion, but it's extra work to do so, and it's not clear
59-
// that there would be enough user benefit to justify the work).
60-
c?._field.expectStaticType<Exactly<Object?>>();
56+
// After the cascade, the promotion is retained, because in the implicit
57+
// control flow join implied by the `?..`, the control flow path that skips
58+
// the promotion is dead.
59+
c?._field.expectStaticType<Exactly<Object>>();
6160
// [error column 4, length 2]
6261
// [analyzer] STATIC_WARNING.INVALID_NULL_AWARE_OPERATOR
6362
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
// Tests the interactions between field promotion and various forms of
6+
// null-aware accesses, when `sound-flow-analysis` is enabled.
7+
8+
// Note that there is (as of this writing) no specification for field promotion;
9+
// this test reflects the current behavior of the `sound-flow-analysis` feature,
10+
// as discussed in https://github.com/dart-lang/language/issues/4344.
11+
12+
// SharedOptions=--enable-experiment=sound-flow-analysis
13+
14+
// ignore_for_file: invalid_null_aware_operator
15+
16+
import 'package:expect/static_type_helper.dart';
17+
18+
class A {
19+
final int? _i;
20+
A(this._i);
21+
}
22+
23+
// Inside a non-cascaded null-aware field access, prior promotions take effect.
24+
void nonCascadedPriorPromotion(A a) {
25+
if (a._i != null) {
26+
// `a._i` is promoted now.
27+
a._i.expectStaticType<Exactly<int>>();
28+
// And so is `a?._i`.
29+
a?._i.expectStaticType<Exactly<int>>();
30+
}
31+
}
32+
33+
// A promotion that occurs inside a non-cascaded null-aware field access
34+
// produces a continued effect after the field access.
35+
void nonCascadedPromotionAfter(A a) {
36+
a?._i!;
37+
// `a._i` is now promoted.
38+
a._i.expectStaticType<Exactly<int>>();
39+
// And so is `a?._i`.
40+
a?._i.expectStaticType<Exactly<int>>();
41+
}
42+
43+
// Inside a cascaded null-aware field access, prior promotions take effect.
44+
void cascadedPriorPromotion(A a) {
45+
if (a._i != null) {
46+
// `a._i` is promoted now.
47+
a._i.expectStaticType<Exactly<int>>();
48+
// And `a?.._i` is promoted.
49+
a?.._i.expectStaticType<Exactly<int>>();
50+
}
51+
}
52+
53+
// A promotion that occurs inside a cascaded null-aware field access produces a
54+
// continued effect after the cascade (assuming the target is non-nullable).
55+
void cascadedPromotionAfter(A a) {
56+
// `a._i` is promoted inside the cascade
57+
a
58+
?.._i!
59+
.._i.expectStaticType<Exactly<int>>();
60+
// And the promotion is retained afterwards.
61+
a._i.expectStaticType<Exactly<int>>();
62+
}
63+
64+
// A promotion that occurs inside a cascaded null-aware field access produces an
65+
// effect on the value of the cascade expression (assuming the target is
66+
// non-nullable).
67+
void cascadedPromotionValue(A a) {
68+
// `a._i` is promoted inside the cascade
69+
(a
70+
?.._i!
71+
.._i.expectStaticType<Exactly<int>>())
72+
// And the promotion is retained in the value of the cascade expression.
73+
._i
74+
.expectStaticType<Exactly<int>>();
75+
}
76+
77+
main() {
78+
nonCascadedPriorPromotion(A(0));
79+
nonCascadedPromotionAfter(A(0));
80+
cascadedPriorPromotion(A(0));
81+
cascadedPromotionAfter(A(0));
82+
cascadedPromotionValue(A(0));
83+
}

0 commit comments

Comments
 (0)