-
Notifications
You must be signed in to change notification settings - Fork 4.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Lambda expression explicit return type: binding #54075
Conversation
} | ||
|
||
[Fact] | ||
public void LambdaReturnType_02() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add a similar test, but with one of the type parameters constrained by the other type parameter (where T : U
for example)? #Closed
// At this point we know that we have either a delegate type or an expression type for the target. | ||
if (reason == LambdaConversionResult.MismatchedReturnType) | ||
{ | ||
Error(diagnostics, ErrorCode.ERR_CantConvAnonMethReturnType, syntax, id, targetType); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: The name of the error code suggests that conversions have something to do, but the logic and message are that the types have to match. Consider renaming the error code and diagnostic. #Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Never mind. I see that we're using same naming convention for parameter types (ERR_CantConvAnonMethParams
). Fine as-is then
if (anonymousFunction.HasExplicitReturnType(out var refKind, out var returnType)) | ||
{ | ||
if (invokeMethod.RefKind != refKind || | ||
!invokeMethod.ReturnType.Equals(returnType.Type, TypeCompareKind.AllIgnoreOptions)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we sure this is the rule we want? Does this fall out of existing spec rules, or needs to be spec'ed?
On one hand, Func<int> f = localFunctionReturningShort;
an error (with short localFunctionReturningShort()
). So Func<int> f = short () => 1;
makes sense to error as well.
On the other hand, I think Func<object, int> f = (o) => 1;
involves a conversion from expression which gives o
its object
type, so why wouldn't that conversion also handle Func<object, int> f = short (o) => 1
? #Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, is AllIgnoreOptions
the right option? Can we include tests for types differing by small differences (object vs. dynamic, nullability, custom modifiers)?
Update: I see LambdaReturnType_04
covers most of those. Is there a way to get into a situation with a custom modifiers difference as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we sure this is the rule we want? Does this fall out of existing spec rules, or needs to be spec'ed?
This should be confirmed with LDM, but I believe it corresponds to the behavior for parameters. For instance, Action<string> a = (object o) => { };
results in a conversion error (see sharplab.io).
Can we include tests for types differing by small differences (object vs. dynamic, nullability, custom modifiers)?
There are tests for object
and dynamic
, tuple element names, nint
and IntPtr
, and nullable differences.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're correct, this is the behavior for parameters: https://github.com/dotnet/csharplang/blob/main/spec/conversions.md#anonymous-function-conversions
"If F has an explicitly typed parameter list, each parameter in D has the same type and modifiers as the corresponding parameter in F."
We probably need to add a similar rule to the spec for the return type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These things should just fall out. Variance only allows for implicit reference conversions, nothing else, because they need to be representation-preserving.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a way to get into a situation with a custom modifiers difference as well?
Added tests for custom modifiers.
MessageID.IDS_FeatureLambdaReturnType.CheckFeatureAvailability(diagnostics, syntax); | ||
|
||
RefKind refKind = RefKind.None; | ||
if (syntax is RefTypeSyntax refTypeSyntax) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: There's a helper that might be re-used here: syntax = syntax.ReturnType.SkipRef(out RefKind refKind);
#Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated, thanks.
static void Main() | ||
{ | ||
Delegate d; | ||
d = (ref void () => { }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's add a passing test involving a void-returning lambda too. #Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I couldn't find the test you added to address this comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I updated DelegateTypeTests.GetLambdaData()
which is used for tests that compile and report the signature of the lambdas.
declarationMap.Add(s.Name, s); | ||
var name = parameter.Name; | ||
this.parameterMap.Add(name, parameter); | ||
if (!_definitionMap.ContainsKey(name)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need the IsDiscard
check after all (either on parameterMap
or on _definitionMap
)? #Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, missed it above ;-)
@@ -165,6 +165,7 @@ public TypeWithAnnotations GetInferredReturnType(ConversionsBase? conversions, N | |||
internal static InferredLambdaReturnType InferReturnType(ArrayBuilder<(BoundReturnStatement, TypeWithAnnotations)> returnTypes, | |||
UnboundLambda node, Binder binder, TypeSymbol? delegateType, bool isAsync, ConversionsBase conversions) | |||
{ | |||
Debug.Assert(!node.HasExplicitReturnType(out _, out _)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider moving assertion into InferReturnTypeImpl
so that it also covers the InferReturnType
overload a few lines above (line 159) #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ping in case you missed this suggestion
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. There's more work required to move this assert. I'll investigate.
I assume In reply to: 861226670 In reply to: 861226670 In reply to: 861226670 Refers to: src/Compilers/CSharp/Portable/BoundTree/UnboundLambda.cs:858 in f8f6c7a. [](commit_id = f8f6c7a, deletion_comment = False) |
nit: since you enabled nullability in this section of the file, this local should probably be annotated since we're null-testing it below. #Resolved In reply to: 861231753 In reply to: 861231753 Refers to: src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs:1208 in f8f6c7a. [](commit_id = f8f6c7a, deletion_comment = False) |
@@ -183,6 +183,9 @@ public static bool IsInTypeOnlyContext(ExpressionSyntax node) | |||
case LocalFunctionStatement: | |||
return ((LocalFunctionStatementSyntax)parent).ReturnType == node; | |||
|
|||
case ParenthesizedLambdaExpression: | |||
return ((ParenthesizedLambdaExpressionSyntax)parent).ReturnType == node; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What scenario did this affect? #Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is required to bind dynamic
as an explicit return type since BindNonGenericSimpleNamespaceOrTypeOrAliasSymbol()
requires dynamic
to be in a type-only context.
@@ -2524,7 +2540,7 @@ private bool TryGetReturnType(out TypeWithAnnotations type, out FlowAnalysisAnno | |||
return false; | |||
} | |||
|
|||
var delegateOrMethod = _delegateInvokeMethod ?? method; | |||
var delegateOrMethod = _useDelegateInvokeReturnType ? _delegateInvokeMethod! : method; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: we should probably also have an assertion (not just a suppression) since this is enforced elsewhere #Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could although we're dereferencing the result immediately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, had missed that. Sounds okay then
@@ -6566,6 +6590,15 @@ private void ReportNullabilityMismatchWithTargetDelegate(Location location, Name | |||
} | |||
|
|||
Debug.Assert(delegateType is object); | |||
|
|||
if (unboundLambda.HasExplicitReturnType(out _, out var returnType) && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we take the refKind into account too? #Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're already requiring an identity conversion so I don't think ref kind will have any effect. Added a test in case the identity conversion requirement is relaxed.
@@ -6566,6 +6590,15 @@ private void ReportNullabilityMismatchWithTargetDelegate(Location location, Name | |||
} | |||
|
|||
Debug.Assert(delegateType is object); | |||
|
|||
if (unboundLambda.HasExplicitReturnType(out _, out var returnType) && | |||
IsNullabilityMismatch(invoke.ReturnTypeWithAnnotations, returnType, requireIdentity: true)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be requireIdentity: false
instead? #Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Requiring the return type nullability to match exactly corresponds to the existing requirement for parameters (see sharplab.io).
We could relax this requirement but we should probably change the requirement for both parameters and return types at the same time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. It makes sense to follow what we do for parameters
}"; | ||
var comp = CreateCompilation(sourceB, references: new[] { refA }, parseOptions: TestOptions.RegularPreview); | ||
comp.VerifyDiagnostics( | ||
// (7,11): error CS0648: 'A' is a type not supported by the language |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't the diagnostic I expected for a use-site issue, this looks more like bad PE. Should the test name be adjusted?
If so, could you add a use-site error scenario? #Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a use-site diagnostic reported in PENamedTypeSymbol.GetUseSiteDiagnosticImpl()
.
// (9,4): error CS0182: An attribute argument must be a constant expression, typeof expression or array creation expression of an attribute parameter type | ||
// [A(F(async () => await Task.Yield()))] | ||
Diagnostic(ErrorCode.ERR_BadAttributeArgument, "F(async () => await Task.Yield())").WithLocation(9, 4)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider testing a type inference scenario with an explicit return type.
M(int (i) => i)
with M<T>(Func<T, T> f)
would be interesting.
Or something more basic like: M(int (o) => 1)
with M<T>(Func<object, T> f)
. #Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added those tests, although currently type inference makes explicit type inferences from anonymous method parameter types but not return type.
{ | ||
static void F<T>() | ||
{ | ||
Func<T> f1 = T () => default; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider testing some best type scenarios involving multiple lambdas: b ? (ExplicitType1 () => null) : (ExplicitType2 () => null)
#Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done with review pass (iteration 3)
{ | ||
var source = | ||
@"delegate T D1<T>(ref T t); | ||
delegate ref T D2<T>(ref T t); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider adding a test to ensure that ref readonly
returns work as well. #Resolved
This method is still used when the return type is explicit. (It is the call to That said, it's possible we could avoid this code path altogether for explicit return types. #54125 In reply to: 861226670 Refers to: src/Compilers/CSharp/Portable/BoundTree/UnboundLambda.cs:858 in f8f6c7a. [](commit_id = f8f6c7a, deletion_comment = False) |
} | ||
|
||
// Should method type inference make an explicit type inference | ||
// from the lambda expression return type? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI, I added a note to test plan to track this: "LDM: Should the explicit return type contribute to type inference (see TypeInference_02
) "
nit: I'd probably also leave a PROTOTYPE marker here #Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM Thanks (iteration 4) with a minor suggestion to move an assertion
{ | ||
diagnostics.Add(ErrorFacts.GetStaticClassReturnCode(useWarning: false), syntax.Location, type); | ||
} | ||
else if (returnType.IsRestrictedType(ignoreSpanLikeTypes: true)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Theoretically, when we start inferring delegate types, we could encounter an async function with an __arglist
parameter. Do we need to check that here? Or perhaps assert that we'll never encounter such a scenario? SourceOrdinaryMethodSymbolBase checks this case in its equivalent method, so I think we should do something about it, even if it's just to assert we'll never hit it. #Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this an issue introduced by explicit return types or an existing issue with lambda parameters?
It looks like the parser rejects __arglist
in IsPossibleLambdaParameter()
. I've added some parsing tests.
var returnTypes = ArrayBuilder<(BoundReturnStatement, TypeWithAnnotations)>.GetInstance(); | ||
BoundLambda.BlockReturns.GetReturnTypes(returnTypes, block); | ||
inferredReturnType = BoundLambda.InferReturnType(returnTypes, _unboundLambda, lambdaBodyBinder, delegateType, lambdaSymbol.IsAsync, lambdaBodyBinder.Conversions); | ||
// TODO: Should InferredReturnType.UseSiteDiagnostics be merged into BoundLambda.Diagnostics? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would think so. #ByDesign
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll leave this as is since this is an existing comment.
@@ -235,6 +238,8 @@ internal Location DiagnosticLocation | |||
} | |||
} | |||
|
|||
private bool HasExplicitReturnType => (Syntax as ParenthesizedLambdaExpressionSyntax)?.ReturnType is not null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
} | ||
|
||
for (NamedTypeSymbol curr = this.ContainingType; (object)curr != null; curr = curr.ContainingType) | ||
// Avoid checking attributes on containing types to avoid a potential cycle when a lambda | ||
// is used in an attribute argument - see https://github.com/dotnet/roslyn/issues/54074. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This issue is closed as won't fix: consider updating this comment to indicate that since partial trust isn't supported any more, we don't want to make the effort to plumb through understanding whether we're currently in an attribute here. #Closed
static void Main() | ||
{ | ||
Func<dynamic> f1 = object () => default!; | ||
Func<(int, int)> f2 = (int X, int Y) () => default; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't report diagnostics for simple tuple assignments or for method group conversions involving tuple return types, and the lambda cases seem similar to those (see sharplab.io).
static void Main() | ||
{ | ||
Expression<Func<object>> e1 = dynamic () => default!; | ||
Expression<Func<(int X, int Y)>> e2 = (int, int) () => default; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment #ByDesign
Done review pass (commit 5) |
} | ||
|
||
return null; | ||
return Binder.GetMethodGroupOrLambdaDelegateType( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we push this logic down into
GetMethodGroupOrLambdaDelegateType
?
I considered that but decided against it because that would require GetMethodGroupOrLambdaDelegateType
to allow two ways to represent System.Void
- either null
or an actual TypeSymbol
- because InferReturnType()
will return null
for that case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM Thanks (iteration 8)
Support optional explicit return type for lambda expressions with parenthesized parameter list:
Proposal: lambda-improvements.md
Test plan: #52192