Support promotion of instance variables with anonymous methods

This CL adds support for promotion of certain private final instance
variables along with anonymous methods. The promotions do not differ
from the ones which are already available in Dart without anonymous
methods, but it requires some generalizations to handle the changing
value of `this` which is made possible by anonymous methods.

Change-Id: I720a5fa6d29a8a7d19bb2e167dc135f97492b525
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/498840
Reviewed-by: Paul Berry <paulberry@google.com>
This commit is contained in:
Erik Ernst
2026-05-07 05:39:13 -07:00
parent d2903a568c
commit 1b75e2701c
9 changed files with 367 additions and 24 deletions
@@ -750,6 +750,7 @@ abstract class FlowAnalysis<
required bool isFinal,
required bool isLate,
required bool isImplicitlyTyped,
bool inheritPromotableProperties = false,
});
/// Whether the [variable] is definitely assigned in the current state.
@@ -1195,6 +1196,12 @@ abstract class FlowAnalysis<
SharedTypeView scrutineeType,
);
/// Call this method just before changing the binding of `this`.
void thisBinding_begin(ExpressionInfo? targetInfo);
/// Call this method just after the end of a `this` binding.
void thisBinding_end();
/// Call this method just after visiting the expression `this` (or the
/// pseudo-expression `super`, in the case of the analyzer, which represents
/// `super.x` as a property get whose target is `super`).
@@ -1956,11 +1963,13 @@ class FlowAnalysisDebug<
required bool isFinal,
required bool isLate,
required bool isImplicitlyTyped,
bool inheritPromotableProperties = false,
}) {
_wrap(
'initialize($variable, $matchedType, $initializerExpressionInfo, '
'isFinal: $isFinal, isLate: $isLate, '
'isImplicitlyTyped: $isImplicitlyTyped)',
'isImplicitlyTyped: $isImplicitlyTyped, '
'inheritPromotableProperties: $inheritPromotableProperties)',
() => _wrapped.initialize(
variable,
matchedType,
@@ -1968,6 +1977,7 @@ class FlowAnalysisDebug<
isFinal: isFinal,
isLate: isLate,
isImplicitlyTyped: isImplicitlyTyped,
inheritPromotableProperties: inheritPromotableProperties,
),
);
}
@@ -2498,6 +2508,19 @@ class FlowAnalysisDebug<
);
}
@override
void thisBinding_begin(ExpressionInfo? targetInfo) {
_wrap(
'thisBinding_begin($targetInfo)',
() => _wrapped.thisBinding_begin(targetInfo),
);
}
@override
void thisBinding_end() {
_wrap('thisBinding_end()', () => _wrapped.thisBinding_end());
}
@override
ExpressionInfo thisOrSuper(
SharedTypeView staticType, {
@@ -5173,8 +5196,10 @@ class _FlowAnalysisImpl<
@override
late final SsaNode _superSsaNode = new SsaNode();
final List<SsaNode> _thisSsaNodes = [new SsaNode()];
@override
late final SsaNode _thisSsaNode = new SsaNode();
SsaNode get _thisSsaNode => _thisSsaNodes.last;
@override
final List<_Reference> _cascadeTargetStack = [];
@@ -5845,6 +5870,7 @@ class _FlowAnalysisImpl<
required bool isFinal,
required bool isLate,
required bool isImplicitlyTyped,
bool inheritPromotableProperties = false,
}) {
SharedTypeView unpromotedType = operations.variableType(variable);
int variableKey = promotionKeyStore.keyForVariable(variable);
@@ -5856,6 +5882,7 @@ class _FlowAnalysisImpl<
isLate: isLate,
isImplicitlyTyped: isImplicitlyTyped,
unpromotedType: unpromotedType,
inheritPromotableProperties: inheritPromotableProperties,
);
}
@@ -6643,6 +6670,24 @@ class _FlowAnalysisImpl<
}
}
@override
void thisBinding_begin(ExpressionInfo? targetInfo) {
_Reference? expressionReference = _getExpressionReference(targetInfo);
SsaNode ssaNode =
expressionReference?.ssaNode ??
new SsaNode(
conditionVariableState: targetInfo != null && targetInfo.isNonTrivial
? targetInfo
: null,
);
_thisSsaNodes.add(ssaNode);
}
@override
void thisBinding_end() {
_thisSsaNodes.removeLast();
}
@override
ExpressionInfo thisOrSuper(
SharedTypeView staticType, {
@@ -7445,6 +7490,7 @@ class _FlowAnalysisImpl<
required bool isLate,
required bool isImplicitlyTyped,
required SharedTypeView unpromotedType,
bool inheritPromotableProperties = false,
}) {
if (isLate) {
// Don't use expression info for late variables, since we don't know when
@@ -7458,12 +7504,15 @@ class _FlowAnalysisImpl<
// https://github.com/dart-lang/language/issues/1785.
expressionInfo = null;
}
SsaNode newSsaNode = new SsaNode(
conditionVariableState:
expressionInfo != null && expressionInfo.isNonTrivial
? expressionInfo
: null,
);
SsaNode newSsaNode =
inheritPromotableProperties && expressionInfo is _Reference
? expressionInfo.ssaNode
: new SsaNode(
conditionVariableState:
expressionInfo != null && expressionInfo.isNonTrivial
? expressionInfo
: null,
);
_current = _current.write(
this,
null,
@@ -7731,16 +7780,23 @@ class _FlowAnalysisImpl<
).restoreConditionVariableState(scrutineeInfo, this, _current);
}
TrivialVariableReference _thisOrSuperReference(
_Reference _thisOrSuperReference(
SharedTypeView staticType, {
required bool isSuper,
}) => new TrivialVariableReference(
promotionKey: promotionKeyStore.thisPromotionKey,
model: _current,
type: staticType,
isThisOrSuper: true,
ssaNode: isSuper ? _superSsaNode : _thisSsaNode,
);
}) {
SsaNode ssaNode = isSuper ? _superSsaNode : _thisSsaNode;
return new TrivialVariableReference(
promotionKey: promotionKeyStore.thisPromotionKey,
model: _current,
type: staticType,
isThisOrSuper: true,
ssaNode: ssaNode,
).restoreConditionVariableState(
ssaNode.conditionVariableState,
this,
_current,
);
}
TrivialVariableReference _variableReference(
int variableKey,
@@ -12827,6 +12827,165 @@ main() {
checkAssigned(branch1, false),
]);
});
test('Anonymous method with target having no ExpressionInfo', () {
h.run([
expr(
'A',
).invokeAnonymousMethod([checkReachable(true)], returnType: 'void'),
]);
});
test('Anonymous method has a different notion of this', () {
h.addMember('C', '_field', 'Object', promotable: true);
h.thisType = 'C';
h.run([
this_.as_('C'),
this_.property('_field').as_('num'),
checkPromoted(this_.property('_field'), 'num'),
expr('C').invokeAnonymousMethod([
checkNotPromoted(this_.property('_field')),
this_.property('_field').as_('int'),
checkPromoted(this_.property('_field'), 'int'),
], returnType: 'void'),
checkPromoted(this_.property('_field'), 'num'),
]);
});
test(
'Anonymous method with target having ExpressionInfo but not a Reference',
() {
h.addMember('C', '_field', 'Object', promotable: true);
h.thisType = 'C';
h.run([
this_.as_('C'),
this_.property('_field').as_('num'),
checkPromoted(this_.property('_field'), 'num'),
expr('bool').conditional(expr('C'), expr('C')).invokeAnonymousMethod([
checkNotPromoted(this_.property('_field')),
this_.property('_field').as_('int'),
checkPromoted(this_.property('_field'), 'int'),
], returnType: 'void'),
checkPromoted(this_.property('_field'), 'num'),
]);
},
);
test('Anonymous method promotes this._field '
'from localVariable._field and vice versa', () {
var x = Var('x');
h.addMember('A', '_field', 'Object', promotable: true);
h.run([
declare(x, type: 'A', initializer: expr('A')),
x.property('_field').as_('num'),
x.invokeAnonymousMethod([
checkPromoted(this_.property('_field'), 'num'),
this_.property('_field').as_('int'),
], returnType: 'void'),
checkPromoted(x.property('_field'), 'int'),
]);
});
test('Anonymous method promotes this._field '
'from instanceVariable._field and vice versa', () {
h.addMember('A', '_field', 'Object', promotable: true);
h.addMember('B', '_subField', 'Object', promotable: true);
h.thisType = 'A';
h.run([
this_.as_('A'),
this_.property('_field').as_('B'),
this_.property('_field').property('_subField').as_('num'),
this_.property('_field').invokeAnonymousMethod([
checkPromoted(this_.property('_subField'), 'num'),
this_.property('_subField').as_('int'),
], returnType: 'void'),
checkPromoted(this_.property('_field').property('_subField'), 'int'),
]);
});
test('Parameterized anonymous method has the same notion of this', () {
h.addMember('C', '_field', 'Object', promotable: true);
h.thisType = 'C';
h.run([
this_.as_('C'),
this_.property('_field').as_('num'),
checkPromoted(this_.property('_field'), 'num'),
expr('C').invokeAnonymousMethod(isParameterless: false, [
checkPromoted(this_.property('_field'), 'num'),
this_.property('_field').as_('int'),
checkPromoted(this_.property('_field'), 'int'),
], returnType: 'void'),
checkPromoted(this_.property('_field'), 'int'),
]);
});
test('Parameterized anonymous method with target having ExpressionInfo '
'but not a Reference', () {
h.addMember('C', '_field', 'Object', promotable: true);
h.thisType = 'C';
h.run([
this_.as_('C'),
this_.property('_field').as_('num'),
checkPromoted(this_.property('_field'), 'num'),
expr('bool')
.conditional(expr('C'), expr('C'))
.invokeAnonymousMethod(isParameterless: false, [
checkPromoted(this_.property('_field'), 'num'),
this_.property('_field').as_('int'),
checkPromoted(this_.property('_field'), 'int'),
], returnType: 'void'),
checkPromoted(this_.property('_field'), 'int'),
]);
});
test('Parameterized anonymous method parameter inherits promotion', () {
var x = Var('x');
var p = Var('p');
h.addMember('A', '_field', 'Object', promotable: true);
h.thisType = 'A';
h.run([
declare(x, type: 'A', initializer: expr('A')),
x.property('_field').as_('num'),
x.invokeAnonymousMethod(isParameterless: false, parameter: p, [
checkPromoted(p.property('_field'), 'num'),
p.property('_field').as_('int'),
], returnType: 'void'),
checkPromoted(x.property('_field'), 'int'),
]);
});
test('Parameterized anonymous method promotes parameter._field '
'from instanceVariable._field and vice versa', () {
var p = Var('p');
h.addMember('A', '_field', 'B', promotable: true);
h.addMember('B', '_subField', 'Object', promotable: true);
h.thisType = 'A';
h.run([
this_.as_('A'),
this_.property('_field').as_('B'),
this_.property('_field').property('_subField').as_('num'),
this_
.property('_field')
.invokeAnonymousMethod(isParameterless: false, parameter: p, [
checkPromoted(p.property('_subField'), 'num'),
p.property('_subField').as_('int'),
], returnType: 'void'),
checkPromoted(this_.property('_field').property('_subField'), 'int'),
]);
});
test('Anonymous method this serves as condition variable', () {
var x = Var('x');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
x.eq(nullLiteral).not.invokeAnonymousMethod(isParameterless: true, [
this_.conditional(
checkPromoted(x, 'int'),
expr('bool'),
),
], returnType: 'bool'),
]);
});
});
}
@@ -2453,17 +2453,26 @@ class InvokeAnonymousMethod extends Expression {
final bool isNullAware;
final bool isParameterless;
final Var? parameter;
InvokeAnonymousMethod._(
this.target,
this.body, {
required this.returnType,
required this.isNullAware,
required this.isParameterless,
this.parameter,
required super.location,
});
@override
void preVisit(PreVisitor visitor) {
target.preVisit(visitor);
if (parameter != null) {
visitor._assignedVariables.declare(parameter!);
}
body.preVisit(visitor);
}
@@ -2484,10 +2493,40 @@ class InvokeAnonymousMethod extends Expression {
if (isNullAware) {
targetResult = h.typeAnalyzer.createNullAwareGuard(target, targetResult);
}
var targetInfo = targetResult.flowAnalysisInfo;
var previousThisType = h._thisType;
if (isParameterless) {
h.flow.thisBinding_begin(targetInfo);
h._thisType = targetResult.type.unwrapTypeView();
}
h.flow.anonymousBlockBody_begin();
if (parameter != null) {
bool isImplicitlyTyped = parameter!._type == null;
if (parameter!._type == null) {
parameter!._type = targetResult.type.unwrapTypeView<Type>();
}
h.flow.declare(
parameter!,
SharedTypeView(parameter!.type),
initialized: false,
);
h.flow.initialize(
parameter!,
targetResult.type,
targetInfo,
isFinal: false,
isLate: false,
isImplicitlyTyped: isImplicitlyTyped,
inheritPromotableProperties: true,
);
}
// Analyze the block, and generate its IR.
body.visit(h);
h.flow.anonymousBlockBody_end();
if (isParameterless) {
h._thisType = previousThisType;
h.flow.thisBinding_end();
}
// Form the IR for the anonymous method invocation.
h.irBuilder.apply(
'anonymous-method',
@@ -4971,6 +5010,8 @@ mixin ProtoExpression
List<ProtoStatement> body, {
required String returnType,
bool isNullAware = false,
bool isParameterless = true,
Var? parameter,
}) {
var location = computeLocation();
return new InvokeAnonymousMethod._(
@@ -4978,6 +5019,8 @@ mixin ProtoExpression
Block._(body, location: location),
returnType: Type(returnType),
isNullAware: isNullAware,
isParameterless: isParameterless,
parameter: parameter,
location: location,
);
}
@@ -7129,6 +7172,7 @@ class _MiniAstTypeAnalyzer
SharedTypeView(thisType),
isSuper: false,
);
flow.storeExpressionInfo(node, flowAnalysisInfo);
return new ExpressionTypeAnalysisResult(
type: SharedTypeView(thisType),
flowAnalysisInfo: flowAnalysisInfo,
@@ -7151,6 +7195,9 @@ class _MiniAstTypeAnalyzer
SharedTypeView(memberType),
);
var promotedType = wrappedPromotedType?.unwrapTypeView();
if (flowAnalysisInfo != null) {
flow.storeExpressionInfo(node, flowAnalysisInfo);
}
return new ExpressionTypeAnalysisResult(
type: SharedTypeView(promotedType ?? memberType),
flowAnalysisInfo: flowAnalysisInfo,
@@ -7249,6 +7296,7 @@ class _MiniAstTypeAnalyzer
) {
var (promotedType, flowAnalysisInfo) = flow.variableRead(variable);
callback?.call(promotedType?.unwrapTypeView());
flow.storeExpressionInfo(node, flowAnalysisInfo);
return new ExpressionTypeAnalysisResult(
type: promotedType ?? SharedTypeView(variable.type),
flowAnalysisInfo: flowAnalysisInfo,
@@ -7843,6 +7891,9 @@ class _MiniAstTypeAnalyzer
member,
SharedTypeView(memberType),
);
if (propertyGetNode != null && flowAnalysisInfo != null) {
flow.storeExpressionInfo(propertyGetNode, flowAnalysisInfo);
}
return ExpressionTypeAnalysisResult(
type: wrappedPromotedType ?? SharedTypeView(memberType),
flowAnalysisInfo: flowAnalysisInfo,
+36 -5
View File
@@ -1904,6 +1904,7 @@ class ResolverVisitor extends ThrowingAstVisitor<void>
node.visitChildren(this);
var returnType = _finishFunctionBodyInference();
flowAnalysis.flow?.anonymousBlockBody_end();
return returnType;
} finally {
_bodyContext = oldBodyContext;
@@ -1916,11 +1917,13 @@ class ResolverVisitor extends ThrowingAstVisitor<void>
TypeImpl? imposedType,
}) {
checkUnreachableNode(node);
analyzeExpression(
node.expression,
SharedTypeSchemaView(imposedType ?? UnknownInferredType.instance),
);
popRewrite();
return node.expression.staticType ?? typeProvider.dynamicType;
}
@@ -1975,11 +1978,33 @@ class ResolverVisitor extends ThrowingAstVisitor<void>
for (var parameter in parameters.parameters) {
var element = parameter.declaredFragment?.element;
if (element != null) {
flow.declare(
element,
SharedTypeView(element.type),
initialized: true,
);
if (parameter == parameters.parameters.first) {
flow.declare(
element,
SharedTypeView(element.type),
initialized: false,
);
flow.initialize(
element,
SharedTypeView(element.type),
target != null
? flowAnalysis.flow?.getExpressionInfo(target)
: null,
isFinal: false,
isLate: false,
isImplicitlyTyped: parameter.type == null,
inheritPromotableProperties: true,
);
} else {
// An error will occur because there are multiple parameters, but
// those extra parameters should still allow for meaningful analysis
// in the body of the anonymous method.
flow.declare(
element,
SharedTypeView(element.type),
initialized: true,
);
}
}
}
}
@@ -1988,9 +2013,15 @@ class ResolverVisitor extends ThrowingAstVisitor<void>
if (parameters == null) {
var oldThisType = _thisType;
_thisType = parameterType;
var target = node.target;
var targetInfo = target != null
? flowAnalysis.flow?.getExpressionInfo(target)
: null;
flowAnalysis.flow?.thisBinding_begin(targetInfo);
try {
returnedType = node.body.resolve(this, contextType);
} finally {
flowAnalysis.flow?.thisBinding_end();
_thisType = oldThisType;
}
} else {
@@ -11868,7 +11868,16 @@ class InferenceVisitorImpl extends InferenceVisitorBase
flowAnalysis.declare(
node.variable,
new SharedTypeView(node.variable.type),
initialized: true,
initialized: false,
);
flowAnalysis.initialize(
node.variable,
new SharedTypeView(node.variable.type),
flowAnalysis.getExpressionInfo(node.variable.initializer!),
isFinal: false,
isLate: false,
isImplicitlyTyped: node.isImplicitlyTyped,
inheritPromotableProperties: true,
);
if (node.isNullAware) {
flow.nullAwareAccess_rightBegin(
@@ -11878,11 +11887,20 @@ class InferenceVisitorImpl extends InferenceVisitorBase
);
}
bool isParameterless = node.variable.isSynthesized;
if (isParameterless) {
flow.thisBinding_begin(
flowAnalysis.getExpressionInfo(node.variable.initializer!),
);
}
ExpressionInferenceResult bodyResult = inferExpression(
node.body,
typeContext,
isVoidAllowed: true,
);
if (isParameterless) {
flow.thisBinding_end();
}
if (node.isNullAware) {
flow.nullAwareAccess_end();
@@ -0,0 +1,17 @@
// Copyright (c) 2026, 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.
// SharedOptions=--enable-experiment=anonymous-methods
import 'package:expect/expect.dart';
void main() {
final int? x = 2;
(x != null).{
Expect.isTrue(this ? x.isEven : false);
};
(x != null).(b) {
Expect.isTrue(b ? x.isEven : false);
};
}
@@ -0,0 +1,13 @@
// Copyright (c) 2026, 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.
// SharedOptions=--enable-experiment=anonymous-methods
import 'package:expect/expect.dart';
void main() {
final int? x = 2;
Expect.isTrue((x != null).=> this ? x.isEven : false);
Expect.isTrue((x != null).(b) => b ? x.isEven : false);
}
@@ -6,7 +6,6 @@
// SharedOptions=--enable-experiment=anonymous-methods
import 'package:expect/expect.dart';
import '../../static_type_helper.dart';
extension on int {
int get g1 => this + '$this'.=> this.length;
@@ -6,7 +6,6 @@
// SharedOptions=--enable-experiment=anonymous-methods
import 'package:expect/expect.dart';
import '../../static_type_helper.dart';
class A {
final int x = 'first'.=> this.length + length;