From 1b75e2701c2fd6267199dd562ba028b4cd51cc2e Mon Sep 17 00:00:00 2001 From: Erik Ernst Date: Thu, 7 May 2026 05:39:13 -0700 Subject: [PATCH] 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 --- .../lib/src/flow_analysis/flow_analysis.dart | 88 ++++++++-- .../flow_analysis/flow_analysis_test.dart | 159 ++++++++++++++++++ pkg/_fe_analyzer_shared/test/mini_ast.dart | 51 ++++++ pkg/analyzer/lib/src/generated/resolver.dart | 41 ++++- .../src/type_inference/inference_visitor.dart | 20 ++- .../block/condition_variable_test.dart | 17 ++ .../expression/condition_variable_test.dart | 13 ++ .../expression/extension_member_test.dart | 1 - .../expression/this_access_test.dart | 1 - 9 files changed, 367 insertions(+), 24 deletions(-) create mode 100644 tests/language/anonymous_methods/block/condition_variable_test.dart create mode 100644 tests/language/anonymous_methods/expression/condition_variable_test.dart diff --git a/pkg/_fe_analyzer_shared/lib/src/flow_analysis/flow_analysis.dart b/pkg/_fe_analyzer_shared/lib/src/flow_analysis/flow_analysis.dart index 0edf67491f7..7d5c28744d7 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 @@ -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 _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, 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 b442da7ee0e..592e526defa 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 @@ -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'), + ]); + }); }); } diff --git a/pkg/_fe_analyzer_shared/test/mini_ast.dart b/pkg/_fe_analyzer_shared/test/mini_ast.dart index 9a0d0c6fd98..26c3c13e958 100644 --- a/pkg/_fe_analyzer_shared/test/mini_ast.dart +++ b/pkg/_fe_analyzer_shared/test/mini_ast.dart @@ -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(); + } + 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 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, diff --git a/pkg/analyzer/lib/src/generated/resolver.dart b/pkg/analyzer/lib/src/generated/resolver.dart index a922fb0206d..c331922079e 100644 --- a/pkg/analyzer/lib/src/generated/resolver.dart +++ b/pkg/analyzer/lib/src/generated/resolver.dart @@ -1904,6 +1904,7 @@ class ResolverVisitor extends ThrowingAstVisitor node.visitChildren(this); var returnType = _finishFunctionBodyInference(); flowAnalysis.flow?.anonymousBlockBody_end(); + return returnType; } finally { _bodyContext = oldBodyContext; @@ -1916,11 +1917,13 @@ class ResolverVisitor extends ThrowingAstVisitor 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 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 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 { diff --git a/pkg/front_end/lib/src/type_inference/inference_visitor.dart b/pkg/front_end/lib/src/type_inference/inference_visitor.dart index eb85e71d815..341d6681894 100644 --- a/pkg/front_end/lib/src/type_inference/inference_visitor.dart +++ b/pkg/front_end/lib/src/type_inference/inference_visitor.dart @@ -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(); diff --git a/tests/language/anonymous_methods/block/condition_variable_test.dart b/tests/language/anonymous_methods/block/condition_variable_test.dart new file mode 100644 index 00000000000..53ee64c54e6 --- /dev/null +++ b/tests/language/anonymous_methods/block/condition_variable_test.dart @@ -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); + }; +} diff --git a/tests/language/anonymous_methods/expression/condition_variable_test.dart b/tests/language/anonymous_methods/expression/condition_variable_test.dart new file mode 100644 index 00000000000..ddbc1dc421f --- /dev/null +++ b/tests/language/anonymous_methods/expression/condition_variable_test.dart @@ -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); +} diff --git a/tests/language/anonymous_methods/expression/extension_member_test.dart b/tests/language/anonymous_methods/expression/extension_member_test.dart index 14385990a7e..014da992eab 100644 --- a/tests/language/anonymous_methods/expression/extension_member_test.dart +++ b/tests/language/anonymous_methods/expression/extension_member_test.dart @@ -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; diff --git a/tests/language/anonymous_methods/expression/this_access_test.dart b/tests/language/anonymous_methods/expression/this_access_test.dart index 1a552356b9b..b9e32883ff1 100644 --- a/tests/language/anonymous_methods/expression/this_access_test.dart +++ b/tests/language/anonymous_methods/expression/this_access_test.dart @@ -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;