Flow analysis: add field promotion support for cascades.
This change updates the flow analysis support for field promotion
(which is not yet switched on by default) so that it supports field
accesses inside cascade expressions. The key moving parts are:
- The type hierarchy `PropertyTarget` (which is used by the client to
tell flow analysis whether the target of a property access is
`this`, `super`, or an ordinary expression) now has a new class,
`CascadePropertyTarget`, to represent the situation where the target
of the property access is an implicit reference to the target of the
innermost enclosing cascade expression.
- Flow analysis has two new methods on its API:
`cascadeExpression_afterTarget` and `cascadeExpression_end`, so that
the client can inform flow analysis when a cascade expression is
being analyzed.
- Flow analysis uses its `_makeTemporaryReference` method to track the
implicit temporary variable that stores the target of cascade
expressions. (This method was developed as part of flow analysis
support for patterns, where it creates the data structures necessary
to track the implicit variables that are created as part of pattern
desugaring).
- The "mini-AST" pseudo-language used by flow analysis unit tests now
has a way to represent cascade expressions and method invocations.
- In addition to unit tests for `_fe_analyzer_shared`, `analyzer`, and
`front_end`, there are new language tests in
`tests/language/inference_update_2` to test cascaded field
promotions in end-to-end fashion.
Bug: https://github.com/dart-lang/language/issues/2020
Change-Id: I21353bbc884ed599cb1739cecfb68ad1d975d18b
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/309220
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Reviewed-by: Johnni Winther <johnniwinther@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
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 661d0194..5e418a5 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
@@ -8,6 +8,18 @@
import '../type_inference/promotion_key_store.dart';
import '../type_inference/type_operations.dart';
+/// [PropertyTarget] representing an implicit reference to the target of the
+/// innermost enclosing cascade expression.
+class CascadePropertyTarget extends PropertyTarget<Never> {
+ static const CascadePropertyTarget singleton =
+ const CascadePropertyTarget._();
+
+ const CascadePropertyTarget._() : super._();
+
+ @override
+ String toString() => 'CascadePropertyTarget()';
+}
+
/// Non-promotion reason describing the situation where a variable was not
/// promoted due to an explicit write to the variable appearing somewhere in the
/// source code.
@@ -205,6 +217,32 @@
/// Call this method when visiting a boolean literal expression.
void booleanLiteral(Expression expression, bool value);
+ /// Call this method just after visiting the target of a cascade expression.
+ /// [target] is the target expression (the expression before the first `..` or
+ /// `?..`), and [targetType] is its static type. [isNullAware] indicates
+ /// whether the cascade expression is null-aware (meaning its first separator
+ /// is `?..` rather than `..`).
+ ///
+ /// Returns the effective type of the target expression during execution of
+ /// the cascade sections (this is either the same as [targetType], or its
+ /// non-nullable equivalent, if [isNullAware] is `true`).
+ ///
+ /// The order of visiting a cascade expression should be:
+ /// - Visit the target
+ /// - Call [cascadeExpression_afterTarget].
+ /// - If this is a null-aware cascade, call [nullAwareAccess_rightBegin].
+ /// - Visit each cascade section
+ /// - If this is a null-aware cascade, call [nullAwareAccess_end].
+ /// - Call [cascadeExpression_end].
+ Type cascadeExpression_afterTarget(Expression target, Type targetType,
+ {required bool isNullAware});
+
+ /// Call this method just after visiting a cascade expression. See
+ /// [cascadeExpression_afterTarget] for details.
+ ///
+ /// [wholeExpression] should be the whole cascade expression.
+ void cascadeExpression_end(Expression wholeExpression);
+
/// Call this method just before visiting a conditional expression ("?:").
void conditional_conditionBegin();
@@ -1154,6 +1192,24 @@
}
@override
+ Type cascadeExpression_afterTarget(Expression target, Type targetType,
+ {required bool isNullAware}) {
+ return _wrap(
+ 'cascadeExpression_afterTarget($target, $targetType, '
+ 'isNullAware: $isNullAware)',
+ () => _wrapped.cascadeExpression_afterTarget(target, targetType,
+ isNullAware: isNullAware),
+ isQuery: true,
+ isPure: false);
+ }
+
+ @override
+ void cascadeExpression_end(Expression wholeExpression) {
+ _wrap('cascadeExpression_end($wholeExpression)',
+ () => _wrapped.cascadeExpression_end(wholeExpression));
+ }
+
+ @override
void conditional_conditionBegin() {
_wrap('conditional_conditionBegin()',
() => _wrapped.conditional_conditionBegin());
@@ -3806,6 +3862,10 @@
/// SSA node representing the implicit variable `this`.
late final SsaNode<Type> _thisSsaNode = new SsaNode<Type>(null);
+ /// Stack of information about the targets of any cascade expressions that are
+ /// currently being visited.
+ final List<_Reference<Type>> _cascadeTargetStack = [];
+
_FlowAnalysisImpl(this.operations, this._assignedVariables,
{required this.respectImplicitlyTypedVarInitializers})
: promotionKeyStore = _assignedVariables.promotionKeyStore {
@@ -3897,6 +3957,54 @@
}
@override
+ Type cascadeExpression_afterTarget(Expression target, Type targetType,
+ {required bool isNullAware}) {
+ // If the cascade is null-aware, then during the cascade sections, the
+ // effective type of the target is promoted to non-null.
+ if (isNullAware) {
+ targetType = operations.promoteToNonNull(targetType);
+ }
+ // Retrieve the SSA node for the cascade target, if one has been created
+ // already, so that field accesses within cascade sections will receive the
+ // benefit of previous field promotions. If an SSA node for the target
+ // hasn't been created yet (e.g. because it's not a read of a local
+ // variable), create a fresh SSA node for it, so that field promotions that
+ // occur during cascade sections will persist in later cascade sections.
+ _Reference<Type>? expressionReference = _getExpressionReference(target);
+ SsaNode<Type> ssaNode = expressionReference?.ssaNode ?? new SsaNode(null);
+ // Create a temporary reference to represent the implicit temporary variable
+ // that holds the cascade target. It is important that this is different
+ // from `expressionReference`, because if the target is a local variable,
+ // and that variable is written during one of the cascade sections, future
+ // cascade sections should still be understood to act on the value the
+ // variable had before the write. (e.g. in
+ // `x.._field!.f(x = g()).._field.h()`, no `!` is needed on the second
+ // access to `_field`, even though `x` has been written to).
+ _cascadeTargetStack.add(_makeTemporaryReference(ssaNode, targetType));
+ // Calling `_getExpressionReference` had the effect of clearing
+ // `_expressionReference` (because normally the caller doesn't pass the same
+ // expression to flow analysis twice, so the expression reference isn't
+ // needed anymore). However, in the case of null-aware cascades, this call
+ // will be followed by a call to [nullAwareAccess_rightBegin], and the
+ // expression reference will be needed again. So store it back.
+ if (expressionReference != null) {
+ _storeExpressionReference(target, expressionReference);
+ }
+ return targetType;
+ }
+
+ @override
+ void cascadeExpression_end(Expression wholeExpression) {
+ // Pop the reference for the temporary variable that holds the target of the
+ // cascade stack, and store it as the reference for `wholeExpression`. This
+ // ensures that field accesses performed on the whole cascade expression
+ // (e.g. `(x..f())._field` will still receive the benefit of field
+ // promotion.
+ _Reference<Type> targetInfo = _cascadeTargetStack.removeLast();
+ _storeExpressionReference(wholeExpression, targetInfo);
+ }
+
+ @override
void conditional_conditionBegin() {
_current = _current.split();
}
@@ -5205,7 +5313,9 @@
String propertyName,
Object? propertyMember,
Type staticType) {
- SsaNode<Type>? targetSsaNode;
+ // Find the SSA node for the target of the property access, and figure out
+ // whether the property in question is promotable.
+ SsaNode<Type> targetSsaNode;
bool isPromotable = propertyMember != null &&
operations.isPropertyPromotable(propertyMember);
switch (target) {
@@ -5213,13 +5323,23 @@
targetSsaNode = _superSsaNode;
case ThisPropertyTarget():
targetSsaNode = _thisSsaNode;
+ case CascadePropertyTarget():
+ targetSsaNode = _cascadeTargetStack.last.ssaNode;
case ExpressionPropertyTarget(:var expression):
_Reference<Type>? targetReference = _getExpressionReference(expression);
if (targetReference == null) return null;
- targetSsaNode = targetReference.ssaNode;
+ // If `targetReference` refers to a non-promotable property or variable,
+ // then the result of the property access is also non-promotable (e.g.
+ // `x._nonFinalField._finalField` is not promotable, because
+ // `_nonFinalField` might change at any time). Note that even though the
+ // control flow paths for `SuperPropertyTarget` and `ThisPropertyTarget`
+ // skip this code, we still need to check `isThisOrSuper`, because
+ // `ThisPropertyTarget` is only used for property accesses via
+ // *implicit* `this`.
if (!targetReference.isPromotable && !targetReference.isThisOrSuper) {
isPromotable = false;
}
+ targetSsaNode = targetReference.ssaNode;
}
_PropertySsaNode<Type> propertySsaNode =
targetSsaNode.getProperty(propertyName, promotionKeyStore);
@@ -5667,6 +5787,14 @@
void booleanLiteral(Expression expression, bool value) {}
@override
+ Type cascadeExpression_afterTarget(Expression target, Type targetType,
+ {required bool isNullAware}) =>
+ targetType;
+
+ @override
+ void cascadeExpression_end(Expression wholeExpression) {}
+
+ @override
void conditional_conditionBegin() {}
@override
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 125cfb1..9771bdc 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
@@ -6362,6 +6362,177 @@
]),
]);
});
+
+ 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.expr.property('_field').as_('int').stmt,
+ checkPromoted(x.expr.property('_field'), 'int'),
+ x.expr.cascade([
+ (v) => v.property('_field').checkType('int'),
+ (v) => v.property('_field').checkType('int'),
+ ]).stmt,
+ ]);
+ });
+
+ 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.expr.property('_field').as_('int').stmt,
+ checkPromoted(x.expr.property('_field'), 'int'),
+ x.expr
+ .cascade([(v) => v.property('_field').checkType('int')])
+ .property('_field')
+ .checkType('int')
+ .stmt,
+ ]);
+ });
+
+ test('a cascade expression is not promotable', () {
+ var x = Var('x');
+ h.run([
+ declare(x, initializer: expr('int?')),
+ x.expr
+ .cascade([(v) => v.invokeMethod('toString', [])])
+ .nonNullAssert
+ .stmt,
+ 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')
+ .stmt,
+ ]);
+ });
+
+ 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')).stmt,
+ ]),
+ x.expr
+ .cascade([
+ (v) => v.property('_field').checkType('int?').nonNullAssert,
+ (v) => v.property('_field').checkType('int'),
+ ])
+ .property('_field')
+ .checkType('int')
+ .stmt,
+ ]);
+ });
+ });
+
+ 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.expr.property('_field').as_('int').stmt,
+ checkPromoted(x.expr.property('_field'), 'int'),
+ x.expr.cascade(isNullAware: true, [
+ (v) => v.property('_field').checkType('int'),
+ (v) => v.property('_field').checkType('int'),
+ ]).stmt,
+ ]);
+ });
+
+ 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.expr.property('_field').as_('int').stmt,
+ checkPromoted(x.expr.property('_field'), 'int'),
+ x.expr
+ .cascade(
+ isNullAware: true,
+ [(v) => v.property('_field').checkType('int')])
+ .property('_field')
+ .checkType('int')
+ .stmt,
+ ]);
+ });
+
+ test('a cascade expression is not promotable', () {
+ var x = Var('x');
+ h.run([
+ declare(x, initializer: expr('int?')),
+ x.expr
+ .cascade(
+ isNullAware: true, [(v) => v.invokeMethod('toString', [])])
+ .nonNullAssert
+ .stmt,
+ 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?')
+ .stmt,
+ ]);
+ });
+
+ 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')).stmt,
+ ]),
+ x.expr
+ .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?')
+ .stmt,
+ ]);
+ });
+ });
+ });
});
group('Patterns:', () {
diff --git a/pkg/_fe_analyzer_shared/test/mini_ast.dart b/pkg/_fe_analyzer_shared/test/mini_ast.dart
index eb82a4d..4d65b41 100644
--- a/pkg/_fe_analyzer_shared/test/mini_ast.dart
+++ b/pkg/_fe_analyzer_shared/test/mini_ast.dart
@@ -8,10 +8,12 @@
/// analysis testing.
import 'package:_fe_analyzer_shared/src/flow_analysis/flow_analysis.dart'
show
+ CascadePropertyTarget,
ExpressionInfo,
ExpressionPropertyTarget,
FlowAnalysis,
Operations,
+ PropertyTarget,
SuperPropertyTarget,
ThisPropertyTarget;
import 'package:_fe_analyzer_shared/src/type_inference/assigned_variables.dart';
@@ -574,6 +576,122 @@
}
}
+/// Representation of a cascade expression in the pseudo-Dart language used for
+/// flow analysis testing.
+class Cascade extends Expression {
+ /// The expression appearing before the first `..` (or `?..`).
+ final Expression target;
+
+ /// List of the cascade sections. Each cascade section is an ordinary
+ /// expression, built around a [Property] or [InvokeMethod] expression whose
+ /// target is a [CascadePlaceholder]. See [CascadePlaceholder] for more
+ /// information.
+ final List<Expression> sections;
+
+ /// Indicates whether the cascade is null-aware (i.e. its first section is
+ /// preceded by `?..` instead of `..`).
+ final bool isNullAware;
+
+ Cascade._(this.target, this.sections,
+ {required this.isNullAware, required super.location});
+
+ @override
+ void preVisit(PreVisitor visitor) {
+ target.preVisit(visitor);
+ for (var section in sections) {
+ section.preVisit(visitor);
+ }
+ }
+
+ @override
+ String toString() {
+ return [target, if (isNullAware) '?', ...sections].join('');
+ }
+
+ @override
+ ExpressionTypeAnalysisResult<Type> visit(Harness h, Type context) {
+ // Form the IR for evaluating the LHS
+ var targetType =
+ h.typeAnalyzer.dispatchExpression(target, context).resolveShorting();
+ var previousCascadeTargetIr = h.typeAnalyzer._currentCascadeTargetIr;
+ var previousCascadeType = h.typeAnalyzer._currentCascadeTargetType;
+ // Create a let-variable that will be initialized to the value of the LHS
+ var targetTmp =
+ h.typeAnalyzer._currentCascadeTargetIr = h.irBuilder.allocateTmp();
+ h.typeAnalyzer._currentCascadeTargetType = h.flow
+ .cascadeExpression_afterTarget(target, targetType,
+ isNullAware: isNullAware);
+ if (isNullAware) {
+ h.flow.nullAwareAccess_rightBegin(target, targetType);
+ // Push `targetTmp == null` and `targetTmp` on the IR builder stack,
+ // because they'll be needed later to form the conditional expression that
+ // does the null-aware guarding.
+ h.irBuilder.readTmp(targetTmp, location: location);
+ h.irBuilder.atom('null', Kind.expression, location: location);
+ h.irBuilder.apply(
+ '==', [Kind.expression, Kind.expression], Kind.expression,
+ location: location);
+ h.irBuilder.readTmp(targetTmp, location: location);
+ }
+ // Form the IR for evaluating each section
+ List<MiniIrTmp> sectionTmps = [];
+ for (var section in sections) {
+ h.typeAnalyzer.dispatchExpression(section, h.typeAnalyzer.unknownType);
+ // Create a let-variable that will be initialized to the value of the
+ // section (which will be discarded)
+ sectionTmps.add(h.irBuilder.allocateTmp());
+ }
+ // For the final IR, `let targetTmp = target in let section1Tmp = section1
+ // in section2Tmp = section2 ... in targetTmp`, or, for null-aware cascades,
+ // `let targetTmp = target in targetTmp == null ? targetTmp : let
+ // section1Tmp = section1 in section2Tmp = section2 ... in targetTmp`.
+ h.irBuilder.readTmp(targetTmp, location: location);
+ for (int i = sectionTmps.length; i-- > 0;) {
+ h.irBuilder.let(sectionTmps[i], location: location);
+ }
+ if (isNullAware) {
+ h.irBuilder.apply('if',
+ [Kind.expression, Kind.expression, Kind.expression], Kind.expression,
+ location: location);
+ h.flow.nullAwareAccess_end();
+ }
+ h.irBuilder.let(targetTmp, location: location);
+ h.flow.cascadeExpression_end(this);
+ h.typeAnalyzer._currentCascadeTargetIr = previousCascadeTargetIr;
+ h.typeAnalyzer._currentCascadeTargetType = previousCascadeType;
+ return SimpleTypeAnalysisResult(type: targetType);
+ }
+}
+
+/// Representation of the implicit reference to a cascade target in a cascade
+/// section, in the pseudo-Dart language used for flow analysis testing.
+///
+/// For example, in the cascade expression `x..f()`, the cascade section `..f()`
+/// is represented as an [InvokeMethod] expression whose `target` is a
+/// [CascadePlaceholder].
+class CascadePlaceholder extends Expression {
+ CascadePlaceholder._({required super.location});
+
+ @override
+ void preVisit(PreVisitor visitor) {}
+
+ @override
+ String toString() {
+ // We use an empty string as the string representation of a cascade
+ // placeholder. This ensures that in a cascade expression like `x..f()`, the
+ // cascade section will have the string representation `..f()`.
+ return '.';
+ }
+
+ @override
+ ExpressionTypeAnalysisResult<Type> visit(Harness h, Type context) {
+ h.irBuilder
+ .readTmp(h.typeAnalyzer._currentCascadeTargetIr!, location: location);
+ return SimpleTypeAnalysisResult(
+ type: h.typeAnalyzer._currentCascadeTargetType!);
+ }
+}
+
class CastPattern extends Pattern {
final Pattern inner;
@@ -1198,6 +1316,28 @@
Expression as_(String typeStr) =>
new As._(this, Type(typeStr), location: computeLocation());
+ /// If `this` is an expression `x`, creates a cascade expression with `x` as
+ /// the target, and [sections] as the cascade sections. [isNullAware]
+ /// indicates whether this is a null-aware cascade.
+ ///
+ /// Since each cascade section needs to implicitly refer to the target of the
+ /// cascade, the caller should pass in a closure for each cascade section; the
+ /// closures will be immediately invoked, passing in a [CascadePlaceholder]
+ /// pseudo-expression representing the implicit reference to the cascade
+ /// target.
+ Expression cascade(List<Expression Function(CascadePlaceholder)> sections,
+ {bool isNullAware = false}) {
+ var location = computeLocation();
+ return Cascade._(
+ this,
+ [
+ for (var section in sections)
+ section(CascadePlaceholder._(location: location))
+ ],
+ isNullAware: isNullAware,
+ location: location);
+ }
+
/// Wraps `this` in such a way that, when the test is run, it will verify that
/// the context provided when analyzing the expression matches
/// [expectedContext].
@@ -1234,6 +1374,12 @@
Statement inContext(String context) =>
ExpressionInContext._(this, Type(context), location: computeLocation());
+ /// If `this` is an expression `x`, creates a method invocation with `x` as
+ /// the target, [name] as the method name, and [arguments] as the method
+ /// arguments. Named arguments are not supported.
+ Expression invokeMethod(String name, List<Expression> arguments) =>
+ new InvokeMethod._(this, name, arguments, location: computeLocation());
+
/// If `this` is an expression `x`, creates the expression `x is typeStr`.
///
/// With [isInverted] set to `true`, creates the expression `x is! typeStr`.
@@ -1519,6 +1665,7 @@
'int.>': Type('bool Function(num)'),
'int.>=': Type('bool Function(num)'),
'num.sign': Type('num'),
+ 'Object.toString': Type('String Function()'),
};
final MiniAstOperations _operations = MiniAstOperations();
@@ -1638,10 +1785,26 @@
_PropertyElement? getMember(Type type, String memberName) {
var query = '$type.$memberName';
var member = _members[query];
- if (member == null && !_members.containsKey(query)) {
- fail('Unknown member query: $query');
+ // If an explicit map entry was found for this member, return the associated
+ // value (even if it is `null`; `null` means the test has been explicitly
+ // configured so that the member lookup is supposed to find nothing).
+ if (member != null || _members.containsKey(query)) return member;
+ switch (memberName) {
+ case 'toString':
+ // Assume that all types implement `Object.toString`.
+ return _members['Object.$memberName']!;
+ default:
+ // It's legal to look up any member on the type `dynamic`.
+ if (type.type == 'dynamic') {
+ return null;
+ }
+ // But an attempt to look up an unknown member on any other type
+ // results in a test failure. This is to catch mistakes in unit tests;
+ // if the unit test is deliberately trying to exercise a member lookup
+ // that should find nothing, please use `addMember` to store an
+ // explicit `null` value in the `_members` map.
+ fail('Unknown member query: $query');
}
- return member;
}
/// See [TypeAnalyzer.resolveRelationalPatternOperator].
@@ -1981,6 +2144,40 @@
}
}
+/// Representation of a method invocation in the pseudo-Dart language used for
+/// flow analysis testing.
+class InvokeMethod extends Expression {
+ // The expression appering before the `.`.
+ final Expression target;
+
+ // The name of the method being invoked.
+ final String methodName;
+
+ // The arguments being passed to the invocation.
+ final List<Expression> arguments;
+
+ InvokeMethod._(this.target, this.methodName, this.arguments,
+ {required super.location});
+
+ @override
+ void preVisit(PreVisitor visitor) {
+ target.preVisit(visitor);
+ for (var argument in arguments) {
+ argument.preVisit(visitor);
+ }
+ }
+
+ @override
+ String toString() =>
+ '$target.$methodName(${[for (var arg in arguments) arg].join(', ')})';
+
+ @override
+ ExpressionTypeAnalysisResult<Type> visit(Harness h, Type context) {
+ return h.typeAnalyzer.analyzeMethodInvocation(this,
+ target is CascadePlaceholder ? null : target, methodName, arguments);
+ }
+}
+
class Is extends Expression {
final Expression target;
final Type type;
@@ -3251,14 +3448,15 @@
@override
ExpressionTypeAnalysisResult<Type> visit(Harness h, Type context) {
- return h.typeAnalyzer.analyzePropertyGet(this, target, propertyName);
+ return h.typeAnalyzer.analyzePropertyGet(
+ this, target is CascadePlaceholder ? null : target, propertyName);
}
@override
Type? _getPromotedType(Harness h) {
var receiverType =
h.typeAnalyzer.analyzeExpression(target, h.typeAnalyzer.unknownType);
- var member = h.typeAnalyzer._lookupMember(this, receiverType, propertyName);
+ var member = h.typeAnalyzer._lookupMember(receiverType, propertyName);
return h.flow.promotedPropertyType(
ExpressionPropertyTarget(target), propertyName, member, member!._type);
}
@@ -3670,7 +3868,7 @@
var thisOrSuper = isSuperAccess ? 'super' : 'this';
h.irBuilder.atom('$thisOrSuper.$propertyName', Kind.expression,
location: location);
- var member = h.typeAnalyzer._lookupMember(this, h._thisType!, propertyName);
+ var member = h.typeAnalyzer._lookupMember(h._thisType!, propertyName);
return h.flow.promotedPropertyType(
isSuperAccess
? SuperPropertyTarget.singleton
@@ -4465,6 +4663,16 @@
@override
final TypeAnalyzerOptions options;
+ /// The temporary variable used in the IR to represent the target of the
+ /// innermost enclosing cascade expression, or `null` if no cascade expression
+ /// is currently being visited.
+ MiniIrTmp? _currentCascadeTargetIr;
+
+ /// The type of the target of the innermost enclosing cascade expression
+ /// (promoted to non-nullable, if it's a null-aware cascade), or `null` if no
+ /// cascade expression is currently being visited.
+ Type? _currentCascadeTargetType;
+
_MiniAstTypeAnalyzer(this._harness, this.options);
@override
@@ -4611,6 +4819,38 @@
return new SimpleTypeAnalysisResult<Type>(type: boolType);
}
+ /// Invokes the appropriate flow analysis methods, and creates the IR
+ /// representation, for a method invocation. [node] is the full method
+ /// invocation expression, [target] is the expression before the `.` (or
+ /// `null` in case of a cascaded method invocation), [methodName] is the name
+ /// of the method being invoked, and [arguments] is the list of argument
+ /// expressions.
+ ///
+ /// Null-aware method invocations are not supported. Named arguments are not
+ /// supported.
+ ExpressionTypeAnalysisResult<Type> analyzeMethodInvocation(Expression node,
+ Expression? target, String methodName, List<Expression> arguments) {
+ // Analyze the target, generate its IR, and look up the method's type.
+ var methodType = _handlePropertyTargetAndMemberLookup(
+ null, target, methodName,
+ location: node.location);
+ // Recursively analyze each argument.
+ var inputKinds = [Kind.expression];
+ for (var i = 0; i < arguments.length; i++) {
+ inputKinds.add(Kind.expression);
+ analyzeExpression(
+ arguments[i],
+ methodType is FunctionType
+ ? methodType.positionalParameters[i]
+ : dynamicType);
+ }
+ // Form the IR for the member invocation.
+ _harness.irBuilder.apply(methodName, inputKinds, Kind.expression,
+ location: node.location);
+ // TODO(paulberry): handle null shorting
+ return new SimpleTypeAnalysisResult<Type>(type: methodType);
+ }
+
SimpleTypeAnalysisResult<Type> analyzeNonNullAssert(
Expression node, Expression expression) {
var type = analyzeExpression(expression, unknownType);
@@ -4631,15 +4871,23 @@
return new SimpleTypeAnalysisResult<Type>(type: type);
}
+ /// Invokes the appropriate flow analysis methods, and creates the IR
+ /// representation, for a property get. [node] is the full property get
+ /// expression, [target] is the expression before the `.` (or `null` in the
+ /// case of a cascaded property get), and [propertyName] is the name of the
+ /// property being accessed.
+ ///
+ /// Null-aware property accesses are not supported.
ExpressionTypeAnalysisResult<Type> analyzePropertyGet(
- Expression node, Expression receiver, String propertyName) {
- var receiverType = analyzeExpression(receiver, unknownType);
- var member = _lookupMember(node, receiverType, propertyName);
- var memberType = member?._type ?? dynamicType;
- var promotedType = flow.propertyGet(node,
- ExpressionPropertyTarget(receiver), propertyName, member, memberType);
+ Expression node, Expression? target, String propertyName) {
+ // Analyze the target, generate its IR, and look up the property's type.
+ var propertyType = _handlePropertyTargetAndMemberLookup(
+ node, target, propertyName,
+ location: node.location);
+ // Build the property get IR.
+ _harness.irBuilder.propertyGet(propertyName, location: node.location);
// TODO(paulberry): handle null shorting
- return new SimpleTypeAnalysisResult<Type>(type: promotedType ?? memberType);
+ return new SimpleTypeAnalysisResult<Type>(type: propertyType);
}
void analyzeReturnStatement() {
@@ -4655,7 +4903,7 @@
SimpleTypeAnalysisResult<Type> analyzeThisOrSuperPropertyGet(
Expression node, String propertyName,
{required bool isSuperAccess}) {
- var member = _lookupMember(node, thisType, propertyName);
+ var member = _lookupMember(thisType, propertyName);
var memberType = member?._type ?? dynamicType;
var promotedType = flow.propertyGet(
node,
@@ -5077,7 +5325,7 @@
Type listType(Type elementType) => PrimaryType('List', args: [elementType]);
_PropertyElement? lookupInterfaceMember(
- Node node, Type receiverType, String memberName) {
+ Type receiverType, String memberName) {
return _harness.getMember(receiverType, memberName);
}
@@ -5144,6 +5392,40 @@
return type.recursivelyDemote(covariant: true) ?? type;
}
+ /// Analyzes the target of a property get or method invocation, looks up the
+ /// member being accessed, and returns its type. [propertyGetNode] is the
+ /// source representation of the property get itself (or `null` if this is a
+ /// method invocation), [target] is the source representation of the target
+ /// (or `null` if this is a cascaded access), and [propertyName] is the name
+ /// of the property being accessed. [location] is the source location (used
+ /// for reporting test failures).
+ ///
+ /// Returns the type of the member, or a representation of the type `dynamic`
+ /// if the member couldn't be found.
+ Type _handlePropertyTargetAndMemberLookup(
+ Expression? propertyGetNode, Expression? target, String propertyName,
+ {required String location}) {
+ // Analyze the target, and generate its IR.
+ PropertyTarget<Expression> propertyTarget;
+ Type targetType;
+ if (target == null) {
+ // This is a cascaded access so the IR we need to generate is an implicit
+ // read of the temporary variable holding the cascade target.
+ propertyTarget = CascadePropertyTarget.singleton;
+ _harness.irBuilder.readTmp(_currentCascadeTargetIr!, location: location);
+ targetType = _currentCascadeTargetType!;
+ } else {
+ propertyTarget = ExpressionPropertyTarget(target);
+ targetType = analyzeExpression(target, unknownType);
+ }
+ // Look up the type of the member, applying type promotion if necessary.
+ var member = _lookupMember(targetType, propertyName);
+ var memberType = member?._type ?? dynamicType;
+ return flow.propertyGet(propertyGetNode, propertyTarget, propertyName,
+ member, memberType) ??
+ memberType;
+ }
+
void _irVariables(Node node, Iterable<Var> variables) {
var variableList = variables.toList();
for (var variable in variableList) {
@@ -5158,9 +5440,8 @@
);
}
- _PropertyElement? _lookupMember(
- Expression node, Type receiverType, String memberName) {
- return lookupInterfaceMember(node, receiverType, memberName);
+ _PropertyElement? _lookupMember(Type receiverType, String memberName) {
+ return lookupInterfaceMember(receiverType, memberName);
}
void _visitLoopBody(Statement loop, Statement body) {
diff --git a/pkg/_fe_analyzer_shared/test/mini_ir.dart b/pkg/_fe_analyzer_shared/test/mini_ir.dart
index 2edeb65..edffd16 100644
--- a/pkg/_fe_analyzer_shared/test/mini_ir.dart
+++ b/pkg/_fe_analyzer_shared/test/mini_ir.dart
@@ -272,7 +272,7 @@
/// - Call [let] to build the final `let` expression.
void let(MiniIrTmp tmp, {required String location}) {
_push(IrNode(
- ir: 'let(${tmp._name}, ${tmp._value}, ${_pop(Kind.expression)})',
+ ir: 'let(${tmp._name}, ${tmp._value}, ${_pop(Kind.expression).ir})',
kind: Kind.expression,
location: location));
}
diff --git a/pkg/_fe_analyzer_shared/test/type_inference/type_inference_test.dart b/pkg/_fe_analyzer_shared/test/type_inference/type_inference_test.dart
index 88a4b84..3f683a0 100644
--- a/pkg/_fe_analyzer_shared/test/type_inference/type_inference_test.dart
+++ b/pkg/_fe_analyzer_shared/test/type_inference/type_inference_test.dart
@@ -241,6 +241,36 @@
});
group('Expressions:', () {
+ group('cascade:', () {
+ group('IR:', () {
+ test('not null-aware', () {
+ h.run([
+ expr('dynamic')
+ .cascade([
+ (t) => t.invokeMethod('f', []),
+ (t) => t.invokeMethod('g', [])
+ ])
+ .checkIr('let(t0, expr(dynamic), '
+ 'let(t1, f(t0), let(t2, g(t0), t0)))')
+ .stmt,
+ ]);
+ });
+
+ test('null-aware', () {
+ h.run([
+ expr('dynamic')
+ .cascade(isNullAware: true, [
+ (t) => t.invokeMethod('f', []),
+ (t) => t.invokeMethod('g', [])
+ ])
+ .checkIr('let(t0, expr(dynamic), '
+ 'if(==(t0, null), t0, let(t1, f(t0), let(t2, g(t0), t0))))')
+ .stmt,
+ ]);
+ });
+ });
+ });
+
group('integer literal', () {
test('double context', () {
h.run([
diff --git a/pkg/analyzer/lib/src/dart/resolver/method_invocation_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/method_invocation_resolver.dart
index fc14207..df06867 100644
--- a/pkg/analyzer/lib/src/dart/resolver/method_invocation_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/method_invocation_resolver.dart
@@ -896,7 +896,9 @@
functionExpression = node.methodName;
targetType = _resolver.flowAnalysis.flow?.propertyGet(
functionExpression,
- ThisPropertyTarget.singleton,
+ node.isCascaded
+ ? CascadePropertyTarget.singleton
+ : ThisPropertyTarget.singleton,
node.methodName.name,
node.methodName.staticElement,
getterReturnType) ??
diff --git a/pkg/analyzer/lib/src/dart/resolver/property_element_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/property_element_resolver.dart
index 939ce59..b0f1de0 100644
--- a/pkg/analyzer/lib/src/dart/resolver/property_element_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/property_element_resolver.dart
@@ -483,7 +483,10 @@
result.getter?.returnType ?? _typeSystem.typeProvider.dynamicType;
getType = _resolver.flowAnalysis.flow?.propertyGet(
node,
- ExpressionPropertyTarget(target),
+ isCascaded
+ ? CascadePropertyTarget.singleton
+ as PropertyTarget<Expression>
+ : ExpressionPropertyTarget(target),
propertyName.name,
result.getter,
unpromotedType) ??
diff --git a/pkg/analyzer/lib/src/generated/resolver.dart b/pkg/analyzer/lib/src/generated/resolver.dart
index 263c9fe..6b71dc3 100644
--- a/pkg/analyzer/lib/src/generated/resolver.dart
+++ b/pkg/analyzer/lib/src/generated/resolver.dart
@@ -2070,11 +2070,14 @@
{DartType? contextType}) {
checkUnreachableNode(node);
analyzeExpression(node.target, contextType);
+ var targetType = node.target.staticType ?? typeProvider.dynamicType;
popRewrite();
+ flowAnalysis.flow!.cascadeExpression_afterTarget(node.target, targetType,
+ isNullAware: node.isNullAware);
+
if (node.isNullAware) {
- flowAnalysis.flow!.nullAwareAccess_rightBegin(
- node.target, node.target.staticType ?? typeProvider.dynamicType);
+ flowAnalysis.flow!.nullAwareAccess_rightBegin(node.target, targetType);
_unfinishedNullShorts.add(node.nullShortingTermination);
}
@@ -2083,6 +2086,7 @@
typeAnalyzer.visitCascadeExpression(node, contextType: contextType);
nullShortingTermination(node);
+ flowAnalysis.flow!.cascadeExpression_end(node);
_insertImplicitCallReference(node, contextType: contextType);
nullSafetyDeadCodeVerifier.verifyCascadeExpression(node);
}
diff --git a/pkg/analyzer/test/src/dart/resolution/field_promotion_test.dart b/pkg/analyzer/test/src/dart/resolution/field_promotion_test.dart
index 91a7337..a63b907 100644
--- a/pkg/analyzer/test/src/dart/resolution/field_promotion_test.dart
+++ b/pkg/analyzer/test/src/dart/resolution/field_promotion_test.dart
@@ -5,15 +5,116 @@
import 'package:test_reflective_loader/test_reflective_loader.dart';
import 'context_collection_resolution.dart';
+import 'node_text_expectations.dart';
main() {
defineReflectiveSuite(() {
defineReflectiveTests(FieldPromotionTest);
+ defineReflectiveTests(UpdateNodeTextExpectations);
});
}
@reflectiveTest
class FieldPromotionTest extends PubPackageResolutionTest {
+ test_cascaded_invocation() async {
+ await assertNoErrorsInCode('''
+class C {
+ final Object? _field;
+ C(this._field);
+}
+void f(C c) {
+ c._field as int Function();
+ c.._field().toString();
+}
+''');
+ var node = findNode.functionExpressionInvocation('_field()');
+ assertResolvedNodeText(node, r'''
+FunctionExpressionInvocation
+ function: SimpleIdentifier
+ token: _field
+ staticElement: self::@class::C::@getter::_field
+ staticType: int Function()
+ argumentList: ArgumentList
+ leftParenthesis: (
+ rightParenthesis: )
+ staticElement: <null>
+ staticInvokeType: int Function()
+ staticType: int
+''');
+ }
+
+ test_cascaded_propertyAccess() async {
+ await assertNoErrorsInCode('''
+class C {
+ final Object? _field;
+ C(this._field);
+}
+void f(C c) {
+ c._field as int;
+ c.._field.toString();
+}
+''');
+ var node = findNode.methodInvocation('_field.toString');
+ assertResolvedNodeText(node, r'''
+MethodInvocation
+ target: PropertyAccess
+ operator: ..
+ propertyName: SimpleIdentifier
+ token: _field
+ staticElement: self::@class::C::@getter::_field
+ staticType: int
+ staticType: int
+ operator: .
+ methodName: SimpleIdentifier
+ token: toString
+ staticElement: dart:core::@class::int::@method::toString
+ staticType: String Function()
+ argumentList: ArgumentList
+ leftParenthesis: (
+ rightParenthesis: )
+ staticInvokeType: String Function()
+ staticType: String
+''');
+ }
+
+ test_cascaded_propertyAccess_nullAware() async {
+ await assertNoErrorsInCode('''
+class C {
+ final Object? _field;
+ C(this._field);
+}
+void f(C? c) {
+ c?.._field!.toString().._field.toString();
+ c?._field;
+}
+''');
+ // The `!` in the first statement promotes _field within the cascade
+ assertResolvedNodeText(findNode.propertyAccess('_field.toString'), r'''
+PropertyAccess
+ operator: ..
+ propertyName: SimpleIdentifier
+ token: _field
+ staticElement: self::@class::C::@getter::_field
+ staticType: Object
+ staticType: Object
+''');
+ // But the promotion doesn't last beyond the cascade expression, due to the
+ // implicit control flow join when the `?..` stops taking effect.
+ assertResolvedNodeText(findNode.propertyAccess('c?._field'), r'''
+PropertyAccess
+ target: SimpleIdentifier
+ token: c
+ staticElement: self::@function::f::@parameter::c
+ staticType: C?
+ operator: ?.
+ propertyName: SimpleIdentifier
+ token: _field
+ staticElement: self::@class::C::@getter::_field
+ staticType: Object?
+ staticType: Object?
+''');
+ }
+
test_class_field_invocation_prefixedIdentifier_nullability() async {
await assertNoErrorsInCode('''
class C {
diff --git a/pkg/front_end/lib/src/fasta/type_inference/inference_visitor.dart b/pkg/front_end/lib/src/fasta/type_inference/inference_visitor.dart
index 9fce65f..03d8708 100644
--- a/pkg/front_end/lib/src/fasta/type_inference/inference_visitor.dart
+++ b/pkg/front_end/lib/src/fasta/type_inference/inference_visitor.dart
@@ -148,6 +148,10 @@
coreTypes: coreTypes,
isNonNullableByDefault: isNonNullableByDefault);
+ /// The innermost cascade whose expressions are currently being visited, or
+ /// `null` if no cascade's expressions are currently being visited.
+ Cascade? _enclosingCascade;
+
InferenceVisitorImpl(TypeInferrerImpl inferrer, InferenceHelper helper,
this.constructorDeclaration, this.operations)
: options = new TypeAnalyzerOptions(
@@ -900,11 +904,16 @@
node.variable.initializer = result.expression..parent = node.variable;
node.variable.type = result.inferredType;
+ flowAnalysis.cascadeExpression_afterTarget(
+ result.expression, result.inferredType,
+ isNullAware: node.isNullAware);
NullAwareGuard? nullAwareGuard;
if (node.isNullAware) {
nullAwareGuard = createNullAwareGuard(node.variable);
}
+ Cascade? previousEnclosingCascade = _enclosingCascade;
+ _enclosingCascade = node;
List<ExpressionInferenceResult> expressionResults =
<ExpressionInferenceResult>[];
for (Expression expression in node.expressions) {
@@ -915,6 +924,7 @@
for (int index = 0; index < expressionResults.length; index++) {
body.add(_createExpressionStatement(expressionResults[index].expression));
}
+ _enclosingCascade = previousEnclosingCascade;
Expression replacement = _createBlockExpression(node.variable.fileOffset,
_createBlock(body), createVariableGet(node.variable));
@@ -926,9 +936,24 @@
replacement = new Let(node.variable, replacement)
..fileOffset = node.fileOffset;
}
+ flowAnalysis.cascadeExpression_end(replacement);
return new ExpressionInferenceResult(result.inferredType, replacement);
}
+ @override
+ PropertyTarget<Expression> computePropertyTarget(Expression target) {
+ if (_enclosingCascade case Cascade(:var variable)
+ when target is VariableGet && target.variable == variable) {
+ // `target` is an implicit reference to the target of a cascade
+ // expression; flow analysis uses `CascadePropertyTarget` to represent
+ // this situation.
+ return CascadePropertyTarget.singleton;
+ } else {
+ // `target` is an ordinary expression.
+ return new ExpressionPropertyTarget(target);
+ }
+ }
+
Block _createBlock(List<Statement> statements) {
return new Block(statements);
}
@@ -6635,7 +6660,7 @@
DartType readType = readTarget.getGetterType(this);
readType = flowAnalysis.propertyGet(
propertyGetNode,
- new ExpressionPropertyTarget(receiver),
+ computePropertyTarget(receiver),
propertyName.text,
readTarget.member,
readType) ??
@@ -7651,7 +7676,7 @@
// invocation).
flowAnalysis.propertyGet(
node,
- new ExpressionPropertyTarget(node.receiver),
+ computePropertyTarget(node.receiver),
node.name.text,
propertyGetInferenceResult.member,
readResult.inferredType);
diff --git a/pkg/front_end/lib/src/fasta/type_inference/inference_visitor_base.dart b/pkg/front_end/lib/src/fasta/type_inference/inference_visitor_base.dart
index fd2c88e..51812c3 100644
--- a/pkg/front_end/lib/src/fasta/type_inference/inference_visitor_base.dart
+++ b/pkg/front_end/lib/src/fasta/type_inference/inference_visitor_base.dart
@@ -3129,7 +3129,7 @@
..fileOffset = fileOffset;
calleeType = flowAnalysis.propertyGet(
originalPropertyGet,
- new ExpressionPropertyTarget(originalReceiver),
+ computePropertyTarget(originalReceiver),
originalName.text,
originalTarget,
calleeType) ??
@@ -3241,6 +3241,10 @@
nullAwareGuards);
}
+ /// Computes an appropriate [PropertyTarget] for use in flow analysis to
+ /// represent the given [target].
+ PropertyTarget<Expression> computePropertyTarget(Expression target);
+
/// Performs the core type inference algorithm for method invocations.
ExpressionInferenceResult inferMethodInvocation(
InferenceVisitor visitor,
diff --git a/pkg/front_end/test/spell_checking_list_code.txt b/pkg/front_end/test/spell_checking_list_code.txt
index 1f87a63..a48b153 100644
--- a/pkg/front_end/test/spell_checking_list_code.txt
+++ b/pkg/front_end/test/spell_checking_list_code.txt
@@ -193,6 +193,7 @@
carets
carriage
carrying
+cascade's
cascades
casing
cast
@@ -1074,6 +1075,7 @@
permanently
permit
permits
+persist
pertinent
physically
pi
diff --git a/pkg/front_end/testcases/inference_update_2/cascaded_field_promotion.dart b/pkg/front_end/testcases/inference_update_2/cascaded_field_promotion.dart
new file mode 100644
index 0000000..9f6d9e0
--- /dev/null
+++ b/pkg/front_end/testcases/inference_update_2/cascaded_field_promotion.dart
@@ -0,0 +1,29 @@
+// Copyright (c) 2023, 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 that field promotion logic properly handles cascades.
+
+// SharedOptions=--enable-experiment=inference-update-2
+
+class C {
+ final Object? _field;
+ C(this._field);
+}
+
+void cascadedPropertyAccess(C c) {
+ c._field as int;
+ c.._field.toString();
+}
+
+void cascadedNullAwarePropertyAccess(C? c) {
+ c?.._field!.toString().._field.toString();
+ c?._field;
+}
+
+void cascadedInvocation(C c) {
+ c._field as int Function();
+ c.._field().toString();
+}
+
+main() {}
diff --git a/pkg/front_end/testcases/inference_update_2/cascaded_field_promotion.dart.strong.expect b/pkg/front_end/testcases/inference_update_2/cascaded_field_promotion.dart.strong.expect
new file mode 100644
index 0000000..bd5f70d
--- /dev/null
+++ b/pkg/front_end/testcases/inference_update_2/cascaded_field_promotion.dart.strong.expect
@@ -0,0 +1,30 @@
+library;
+import self as self;
+import "dart:core" as core;
+
+class C extends core::Object {
+ final field core::Object? _field;
+ constructor •(core::Object? _field) → self::C
+ : self::C::_field = _field, super core::Object::•()
+ ;
+}
+static method cascadedPropertyAccess(self::C c) → void {
+ c.{self::C::_field}{core::Object?} as core::int;
+ let final self::C #t1 = c in block {
+ #t1.{self::C::_field}{core::int}.{core::int::toString}(){() → core::String};
+ } =>#t1;
+}
+static method cascadedNullAwarePropertyAccess(self::C? c) → void {
+ let final self::C? #t2 = c in #t2 == null ?{self::C?} null : block {
+ #t2{self::C}.{self::C::_field}{core::Object?}!.{core::Object::toString}(){() → core::String};
+ #t2{self::C}.{self::C::_field}{core::Object}.{core::Object::toString}(){() → core::String};
+ } =>#t2;
+ let final self::C? #t3 = c in #t3 == null ?{core::Object?} null : #t3{self::C}.{self::C::_field}{core::Object?};
+}
+static method cascadedInvocation(self::C c) → void {
+ c.{self::C::_field}{core::Object?} as () → core::int;
+ let final self::C #t4 = c in block {
+ #t4.{self::C::_field}{() → core::int}(){() → core::int}.{core::int::toString}(){() → core::String};
+ } =>#t4;
+}
+static method main() → dynamic {}
diff --git a/pkg/front_end/testcases/inference_update_2/cascaded_field_promotion.dart.textual_outline.expect b/pkg/front_end/testcases/inference_update_2/cascaded_field_promotion.dart.textual_outline.expect
new file mode 100644
index 0000000..07bb84f
--- /dev/null
+++ b/pkg/front_end/testcases/inference_update_2/cascaded_field_promotion.dart.textual_outline.expect
@@ -0,0 +1,9 @@
+class C {
+ final Object? _field;
+ C(this._field);
+}
+
+void cascadedPropertyAccess(C c) {}
+void cascadedNullAwarePropertyAccess(C? c) {}
+void cascadedInvocation(C c) {}
+main() {}
diff --git a/pkg/front_end/testcases/inference_update_2/cascaded_field_promotion.dart.textual_outline_modelled.expect b/pkg/front_end/testcases/inference_update_2/cascaded_field_promotion.dart.textual_outline_modelled.expect
new file mode 100644
index 0000000..2a48b8a
--- /dev/null
+++ b/pkg/front_end/testcases/inference_update_2/cascaded_field_promotion.dart.textual_outline_modelled.expect
@@ -0,0 +1,9 @@
+class C {
+ C(this._field);
+ final Object? _field;
+}
+
+main() {}
+void cascadedInvocation(C c) {}
+void cascadedNullAwarePropertyAccess(C? c) {}
+void cascadedPropertyAccess(C c) {}
diff --git a/pkg/front_end/testcases/inference_update_2/cascaded_field_promotion.dart.weak.expect b/pkg/front_end/testcases/inference_update_2/cascaded_field_promotion.dart.weak.expect
new file mode 100644
index 0000000..bd5f70d
--- /dev/null
+++ b/pkg/front_end/testcases/inference_update_2/cascaded_field_promotion.dart.weak.expect
@@ -0,0 +1,30 @@
+library;
+import self as self;
+import "dart:core" as core;
+
+class C extends core::Object {
+ final field core::Object? _field;
+ constructor •(core::Object? _field) → self::C
+ : self::C::_field = _field, super core::Object::•()
+ ;
+}
+static method cascadedPropertyAccess(self::C c) → void {
+ c.{self::C::_field}{core::Object?} as core::int;
+ let final self::C #t1 = c in block {
+ #t1.{self::C::_field}{core::int}.{core::int::toString}(){() → core::String};
+ } =>#t1;
+}
+static method cascadedNullAwarePropertyAccess(self::C? c) → void {
+ let final self::C? #t2 = c in #t2 == null ?{self::C?} null : block {
+ #t2{self::C}.{self::C::_field}{core::Object?}!.{core::Object::toString}(){() → core::String};
+ #t2{self::C}.{self::C::_field}{core::Object}.{core::Object::toString}(){() → core::String};
+ } =>#t2;
+ let final self::C? #t3 = c in #t3 == null ?{core::Object?} null : #t3{self::C}.{self::C::_field}{core::Object?};
+}
+static method cascadedInvocation(self::C c) → void {
+ c.{self::C::_field}{core::Object?} as () → core::int;
+ let final self::C #t4 = c in block {
+ #t4.{self::C::_field}{() → core::int}(){() → core::int}.{core::int::toString}(){() → core::String};
+ } =>#t4;
+}
+static method main() → dynamic {}
diff --git a/pkg/front_end/testcases/inference_update_2/cascaded_field_promotion.dart.weak.modular.expect b/pkg/front_end/testcases/inference_update_2/cascaded_field_promotion.dart.weak.modular.expect
new file mode 100644
index 0000000..bd5f70d
--- /dev/null
+++ b/pkg/front_end/testcases/inference_update_2/cascaded_field_promotion.dart.weak.modular.expect
@@ -0,0 +1,30 @@
+library;
+import self as self;
+import "dart:core" as core;
+
+class C extends core::Object {
+ final field core::Object? _field;
+ constructor •(core::Object? _field) → self::C
+ : self::C::_field = _field, super core::Object::•()
+ ;
+}
+static method cascadedPropertyAccess(self::C c) → void {
+ c.{self::C::_field}{core::Object?} as core::int;
+ let final self::C #t1 = c in block {
+ #t1.{self::C::_field}{core::int}.{core::int::toString}(){() → core::String};
+ } =>#t1;
+}
+static method cascadedNullAwarePropertyAccess(self::C? c) → void {
+ let final self::C? #t2 = c in #t2 == null ?{self::C?} null : block {
+ #t2{self::C}.{self::C::_field}{core::Object?}!.{core::Object::toString}(){() → core::String};
+ #t2{self::C}.{self::C::_field}{core::Object}.{core::Object::toString}(){() → core::String};
+ } =>#t2;
+ let final self::C? #t3 = c in #t3 == null ?{core::Object?} null : #t3{self::C}.{self::C::_field}{core::Object?};
+}
+static method cascadedInvocation(self::C c) → void {
+ c.{self::C::_field}{core::Object?} as () → core::int;
+ let final self::C #t4 = c in block {
+ #t4.{self::C::_field}{() → core::int}(){() → core::int}.{core::int::toString}(){() → core::String};
+ } =>#t4;
+}
+static method main() → dynamic {}
diff --git a/pkg/front_end/testcases/inference_update_2/cascaded_field_promotion.dart.weak.outline.expect b/pkg/front_end/testcases/inference_update_2/cascaded_field_promotion.dart.weak.outline.expect
new file mode 100644
index 0000000..304ead6
--- /dev/null
+++ b/pkg/front_end/testcases/inference_update_2/cascaded_field_promotion.dart.weak.outline.expect
@@ -0,0 +1,17 @@
+library;
+import self as self;
+import "dart:core" as core;
+
+class C extends core::Object {
+ final field core::Object? _field;
+ constructor •(core::Object? _field) → self::C
+ ;
+}
+static method cascadedPropertyAccess(self::C c) → void
+ ;
+static method cascadedNullAwarePropertyAccess(self::C? c) → void
+ ;
+static method cascadedInvocation(self::C c) → void
+ ;
+static method main() → dynamic
+ ;
diff --git a/pkg/front_end/testcases/modular.status b/pkg/front_end/testcases/modular.status
index bdb1334..e830a41 100644
--- a/pkg/front_end/testcases/modular.status
+++ b/pkg/front_end/testcases/modular.status
@@ -65,6 +65,7 @@
inference_new/infer_assign_to_index_this_upwards: TypeCheckError
inference_new/infer_assign_to_index_upwards: TypeCheckError
inference_update_2/call_invocation_with_hoisting: TypeCheckError
+inference_update_2/cascaded_field_promotion: TypeCheckError
late_lowering/covariant_late_field: TypeCheckError
nnbd/covariant_late_field: TypeCheckError
nnbd/getter_vs_setter_type: TypeCheckError
diff --git a/pkg/front_end/testcases/strong.status b/pkg/front_end/testcases/strong.status
index 3398df8..d06a126 100644
--- a/pkg/front_end/testcases/strong.status
+++ b/pkg/front_end/testcases/strong.status
@@ -143,6 +143,7 @@
inference_new/infer_assign_to_index_this_upwards: TypeCheckError
inference_new/infer_assign_to_index_upwards: TypeCheckError
inference_update_2/call_invocation_with_hoisting: TypeCheckError
+inference_update_2/cascaded_field_promotion: TypeCheckError
late_lowering/covariant_late_field: TypeCheckError
nnbd/covariant_late_field: TypeCheckError
nnbd/getter_vs_setter_type: TypeCheckError
diff --git a/pkg/front_end/testcases/weak.status b/pkg/front_end/testcases/weak.status
index 097d101..67bd2f5 100644
--- a/pkg/front_end/testcases/weak.status
+++ b/pkg/front_end/testcases/weak.status
@@ -25,6 +25,7 @@
inference_update_1/horizontal_inference_extension_method: SemiFuzzFailure # https://dart-review.googlesource.com/c/sdk/+/245004
inference_update_1/horizontal_inference_extension_method: semiFuzzFailureOnForceRebuildBodies # Errors on split
inference_update_2/basic_field_promotion: semiFuzzFailureOnForceRebuildBodies # Private fields.
+inference_update_2/cascaded_field_promotion: TypeCheckError
inference_update_2/disabled: semiFuzzFailureOnForceRebuildBodies # Private fields.
inference_update_2/field_promotion_and_no_such_method: semiFuzzFailureOnForceRebuildBodies # Private fields.
inference_update_2/field_promotion_name_conflicts: semiFuzzFailureOnForceRebuildBodies # Private fields.
diff --git a/tests/language/inference_update_2/cascaded_field_promotion_null_aware_test.dart b/tests/language/inference_update_2/cascaded_field_promotion_null_aware_test.dart
new file mode 100644
index 0000000..a09e561
--- /dev/null
+++ b/tests/language/inference_update_2/cascaded_field_promotion_null_aware_test.dart
@@ -0,0 +1,78 @@
+// Copyright (c) 2023, 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 that field promotion works with null-aware cascades.
+
+// SharedOptions=--enable-experiment=inference-update-2
+
+import '../static_type_helper.dart';
+
+class C {
+ final Object? _field;
+ C([this._field]);
+ void f([_]) {}
+}
+
+void fieldsPromotableWithinCascade(C? c) {
+ // Within a cascade, a field can be promoted using `!`.
+ c
+ ?.._field.expectStaticType<Exactly<Object?>>()
+ .._field!.expectStaticType<Exactly<Object>>()
+ .._field.expectStaticType<Exactly<Object>>();
+ // After the cascade, the promotion is not retained, because of the implicit
+ // control flow join implied by the `?..`. (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).
+ c?._field.expectStaticType<Exactly<Object?>>();
+}
+
+void cascadeExpressionIsNotPromotable(Object? o) {
+ // However, null-checking, casting, or type checking the result of a cascade
+ // expression does not promote the target of the cascade. (It could, in
+ // principle, but it would be extra work to implement, and it seems unlikely
+ // that it would be of much benefit).
+ (o?..toString())!;
+ o.expectStaticType<Exactly<Object?>>();
+ (o?..toString()) as Object;
+ o.expectStaticType<Exactly<Object?>>();
+ if ((o?..toString()) is Object) {
+ o.expectStaticType<Exactly<Object?>>();
+ }
+}
+
+void ephemeralValueFieldsArePromotable(C? Function() getC) {
+ // Fields of an ephemeral value (one that is not explicitly stored in a
+ // variable) can still be promoted in one cascade section, and the results of
+ // the promotion can be seen in later cascade sections.
+ getC()
+ ?.._field.expectStaticType<Exactly<Object?>>()
+ .._field!.expectStaticType<Exactly<Object>>()
+ .._field.expectStaticType<Exactly<Object>>();
+ // But they won't be seen if a fresh value is created.
+ getC()?._field.expectStaticType<Exactly<Object?>>();
+}
+
+void writeCapturedValueFieldsArePromotable(C? c) {
+ // Fields of a write-captured variable can still be promoted in one cascade
+ // section, and the results of the promotion can be seen in later cascade
+ // sections. This is because the target of the cascade is stored in an
+ // implicit temporary variable, separate from the write-captured variable.
+ f() {
+ c = C(null);
+ }
+
+ c
+ ?.._field.expectStaticType<Exactly<Object?>>()
+ .._field!.expectStaticType<Exactly<Object>>()
+ .._field.expectStaticType<Exactly<Object>>();
+ // But fields of the write-captured variable itself aren't promoted.
+ c?._field.expectStaticType<Exactly<Object?>>();
+}
+
+main() {
+ fieldsPromotableWithinCascade(C(0));
+ cascadeExpressionIsNotPromotable(0);
+ ephemeralValueFieldsArePromotable(() => C(0));
+ writeCapturedValueFieldsArePromotable(C(0));
+}
diff --git a/tests/language/inference_update_2/cascaded_field_promotion_test.dart b/tests/language/inference_update_2/cascaded_field_promotion_test.dart
new file mode 100644
index 0000000..bf27d95
--- /dev/null
+++ b/tests/language/inference_update_2/cascaded_field_promotion_test.dart
@@ -0,0 +1,122 @@
+// Copyright (c) 2023, 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 that field promotion works with cascades.
+
+// SharedOptions=--enable-experiment=inference-update-2
+
+import '../static_type_helper.dart';
+
+class C {
+ final Object? _field;
+ C([this._field]);
+ void f([_]) {}
+}
+
+void cascadedAccessReceivesTheBenefitOfPromotion(C c) {
+ // If a field of an object is promoted prior to its use in a cascade, accesses
+ // to the field within cascade sections retain the promotion.
+ c._field as int;
+ c._field.expectStaticType<Exactly<int>>();
+ c
+ .._field.expectStaticType<Exactly<int>>()
+ .._field.expectStaticType<Exactly<int>>();
+ // And the promotion remains on later accesses to the same variable.
+ c._field.expectStaticType<Exactly<int>>();
+}
+
+void fieldAccessOnACascadeExpressionRetainsPromotion(C c) {
+ // If a field of an object is promoted prior to its use in a cascade, accesses
+ // to the field from outside the cascade retain the promotion.
+ c._field as int;
+ c._field.expectStaticType<Exactly<int>>();
+ (c..f())._field.expectStaticType<Exactly<int>>();
+ // And the promotion remains on later accesses to the same variable.
+ c._field.expectStaticType<Exactly<int>>();
+}
+
+void fieldsPromotableWithinCascade(C c) {
+ // Within a cascade, a field can be promoted using `!`.
+ c
+ .._field.expectStaticType<Exactly<Object?>>()
+ .._field!.expectStaticType<Exactly<Object>>()
+ .._field.expectStaticType<Exactly<Object>>();
+ // And after the cascade, the promotion is retained.
+ c._field.expectStaticType<Exactly<Object>>();
+}
+
+void cascadeExpressionIsNotPromotable(Object? o) {
+ // However, null-checking, casting, or type checking the result of a cascade
+ // expression does not promote the target of the cascade. (It could, in
+ // principle, but it would be extra work to implement, and it seems unlikely
+ // that it would be of much benefit).
+ (o..toString())!;
+ o.expectStaticType<Exactly<Object?>>();
+ (o..toString()) as Object;
+ o.expectStaticType<Exactly<Object?>>();
+ if ((o..toString()) is Object) {
+ o.expectStaticType<Exactly<Object?>>();
+ }
+}
+
+void ephemeralValueFieldsArePromotable(C Function() getC) {
+ // Fields of an ephemeral value (one that is not explicitly stored in a
+ // variable) can still be promoted in one cascade section, and the results of
+ // the promotion can be seen in later cascade sections.
+ getC()
+ .._field.expectStaticType<Exactly<Object?>>()
+ .._field!.expectStaticType<Exactly<Object>>()
+ .._field.expectStaticType<Exactly<Object>>();
+ // But they won't be seen if a fresh value is created.
+ getC()._field.expectStaticType<Exactly<Object?>>();
+}
+
+void writeCapturedValueFieldsArePromotable(C c) {
+ // Fields of a write-captured variable can still be promoted in one cascade
+ // section, and the results of the promotion can be seen in later cascade
+ // sections. This is because the target of the cascade is stored in an
+ // implicit temporary variable, separate from the write-captured variable.
+ f() {
+ c = C(null);
+ }
+
+ c
+ .._field.expectStaticType<Exactly<Object?>>()
+ .._field!.expectStaticType<Exactly<Object>>()
+ .._field.expectStaticType<Exactly<Object>>();
+ // But fields of the write-captured variable itself aren't promoted.
+ c._field.expectStaticType<Exactly<Object?>>();
+}
+
+void writeDefeatsLaterAccessesButNotCascadeTarget(C c) {
+ // If a write to a variable happens during a cascade, any promotions based on
+ // that variable are invalidated, but the target of the cascade remains
+ // promoted, since it's stored in an implicit temporarly variable that's
+ // unaffected by the write.
+ c._field as C;
+ c._field.expectStaticType<Exactly<C>>();
+ c
+ .._field.f([c = C(C()), c._field.expectStaticType<Exactly<Object?>>()])
+ .._field.expectStaticType<Exactly<C>>();
+ c._field.expectStaticType<Exactly<Object?>>();
+}
+
+void cascadedInvocationsPermitted(C c) {
+ // A promoted field may be invoked inside a cascade.
+ c._field as int Function();
+ c._field.expectStaticType<Exactly<int Function()>>();
+ c.._field().expectStaticType<Exactly<int>>();
+}
+
+main() {
+ cascadedAccessReceivesTheBenefitOfPromotion(C(0));
+ fieldAccessOnACascadeExpressionRetainsPromotion(C(0));
+ fieldsPromotableWithinCascade(C(0));
+ cascadeExpressionIsNotPromotable(0);
+ ephemeralValueFieldsArePromotable(() => C(0));
+ writeCapturedValueFieldsArePromotable(C(0));
+ writeDefeatsLaterAccessesButNotCascadeTarget(C(C()));
+ int f() => 0;
+ cascadedInvocationsPermitted(C(f));
+}
diff --git a/tests/language/inference_update_2/cascaded_field_promotion_unnecessary_null_aware_error_test.dart b/tests/language/inference_update_2/cascaded_field_promotion_unnecessary_null_aware_error_test.dart
new file mode 100644
index 0000000..1ecdaed
--- /dev/null
+++ b/tests/language/inference_update_2/cascaded_field_promotion_unnecessary_null_aware_error_test.dart
@@ -0,0 +1,149 @@
+// Copyright (c) 2023, 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 that field promotion works with null-aware cascades whose targets are
+// non-nullable (that is, the cascades are "unnecessarily" null-aware).
+//
+// Since the cascades are unnecessarily null-aware, the analyzer produces
+// warnings for it, so this test has to have "error" expectations.
+
+// SharedOptions=--enable-experiment=inference-update-2
+
+import '../static_type_helper.dart';
+
+class C {
+ final Object? _field;
+ C([this._field]);
+ void f([_]) {}
+}
+
+void cascadedAccessReceivesTheBenefitOfPromotion(C c) {
+ // If a field of an object is promoted prior to its use in a cascade, accesses
+ // to the field within cascade sections retain the promotion.
+ c._field as int;
+ c._field.expectStaticType<Exactly<int>>();
+ c
+//^
+// [cfe] Operand of null-aware operation '?..' has type 'C' which excludes null.
+ ?.._field.expectStaticType<Exactly<int>>()
+// ^^^
+// [analyzer] STATIC_WARNING.INVALID_NULL_AWARE_OPERATOR
+ .._field.expectStaticType<Exactly<int>>();
+
+ // And the promotion remains on later accesses to the same variable.
+ c._field.expectStaticType<Exactly<int>>();
+}
+
+void fieldAccessOnACascadeExpressionRetainsPromotion(C c) {
+ // If a field of an object is promoted prior to its use in a cascade, accesses
+ // to the field from outside the cascade retain the promotion.
+ c._field as int;
+ c._field.expectStaticType<Exactly<int>>();
+ (c?..f())._field.expectStaticType<Exactly<int>>();
+// ^
+// [cfe] Operand of null-aware operation '?..' has type 'C' which excludes null.
+// ^^^
+// [analyzer] STATIC_WARNING.INVALID_NULL_AWARE_OPERATOR
+
+ // And the promotion remains on later accesses to the same variable.
+ c._field.expectStaticType<Exactly<int>>();
+}
+
+void fieldsPromotableWithinCascade(C c) {
+ // Within a cascade, a field can be promoted using `!`.
+ c
+//^
+// [cfe] Operand of null-aware operation '?..' has type 'C' which excludes null.
+ ?.._field.expectStaticType<Exactly<Object?>>()
+// ^^^
+// [analyzer] STATIC_WARNING.INVALID_NULL_AWARE_OPERATOR
+ .._field!.expectStaticType<Exactly<Object>>()
+ .._field.expectStaticType<Exactly<Object>>();
+
+ // After the cascade, the promotion is not retained, because of the implicit
+ // control flow join implied by the `?..`. (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).
+ c?._field.expectStaticType<Exactly<Object?>>();
+//^
+// [cfe] Operand of null-aware operation '?.' has type 'C' which excludes null.
+// ^^
+// [analyzer] STATIC_WARNING.INVALID_NULL_AWARE_OPERATOR
+}
+
+void ephemeralValueFieldsArePromotable(C Function() getC) {
+ // Fields of an ephemeral value (one that is not explicitly stored in a
+ // variable) can still be promoted in one cascade section, and the results of
+ // the promotion can be seen in later cascade sections.
+ getC()
+ //^
+ // [cfe] Operand of null-aware operation '?..' has type 'C' which excludes null.
+ ?.._field.expectStaticType<Exactly<Object?>>()
+// ^^^
+// [analyzer] STATIC_WARNING.INVALID_NULL_AWARE_OPERATOR
+ .._field!.expectStaticType<Exactly<Object>>()
+ .._field.expectStaticType<Exactly<Object>>();
+
+ // But they won't be seen if a fresh value is created.
+ getC()._field.expectStaticType<Exactly<Object?>>();
+}
+
+void writeCapturedValueFieldsArePromotable(C c) {
+ // Fields of a write-captured variable can still be promoted in one cascade
+ // section, and the results of the promotion can be seen in later cascade
+ // sections. This is because the target of the cascade is stored in an
+ // implicit temporary variable, separate from the write-captured variable.
+ f() {
+ c = C(null);
+ }
+
+ c
+//^
+// [cfe] Operand of null-aware operation '?..' has type 'C' which excludes null.
+ ?.._field.expectStaticType<Exactly<Object?>>()
+// ^^^
+// [analyzer] STATIC_WARNING.INVALID_NULL_AWARE_OPERATOR
+ .._field!.expectStaticType<Exactly<Object>>()
+ .._field.expectStaticType<Exactly<Object>>();
+
+ // But fields of the write-captured variable itself aren't promoted.
+ c._field.expectStaticType<Exactly<Object?>>();
+}
+
+void writeDefeatsLaterAccessesButNotCascadeTarget(C c) {
+ // If a write to a variable happens during a cascade, any promotions based on
+ // that variable are invalidated, but the target of the cascade remains
+ // promoted, since it's stored in an implicit temporarly variable that's
+ // unaffected by the write.
+ c._field as C;
+ c._field.expectStaticType<Exactly<C>>();
+ c
+//^
+// [cfe] Operand of null-aware operation '?..' has type 'C' which excludes null.
+ ?.._field.f([c = C(C()), c._field.expectStaticType<Exactly<Object?>>()])
+// ^^^
+// [analyzer] STATIC_WARNING.INVALID_NULL_AWARE_OPERATOR
+ .._field.expectStaticType<Exactly<C>>();
+ c._field.expectStaticType<Exactly<Object?>>();
+}
+
+void cascadedInvocationsPermitted(C c) {
+ // A promoted field may be invoked inside a cascade.
+ c._field as int Function();
+ c._field.expectStaticType<Exactly<int Function()>>();
+ c?.._field().expectStaticType<Exactly<int>>();
+//^
+// [cfe] Operand of null-aware operation '?..' has type 'C' which excludes null.
+}
+
+main() {
+ cascadedAccessReceivesTheBenefitOfPromotion(C(0));
+ fieldAccessOnACascadeExpressionRetainsPromotion(C(0));
+ fieldsPromotableWithinCascade(C(0));
+ ephemeralValueFieldsArePromotable(() => C(0));
+ writeCapturedValueFieldsArePromotable(C(0));
+ writeDefeatsLaterAccessesButNotCascadeTarget(C(C()));
+ int f() => 0;
+ cascadedInvocationsPermitted(C(f));
+}