Skip to content
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

Merged
merged 8 commits into from
Jun 21, 2021

Conversation

cston
Copy link
Member

@cston cston commented Jun 14, 2021

Support optional explicit return type for lambda expressions with parenthesized parameter list:

Func<T> f = T () => default;
Action<T> a = static void (T t) => { };

Proposal: lambda-improvements.md
Test plan: #52192

@cston cston marked this pull request as ready for review June 14, 2021 21:00
@cston cston requested a review from a team as a code owner June 14, 2021 21:00
@jcouv jcouv self-assigned this Jun 15, 2021
@jcouv jcouv added this to the C# 10 milestone Jun 15, 2021
}

[Fact]
public void LambdaReturnType_02()
Copy link
Member

@jcouv jcouv Jun 15, 2021

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);
Copy link
Member

@jcouv jcouv Jun 15, 2021

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

Copy link
Member

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))
Copy link
Member

@jcouv jcouv Jun 15, 2021

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

Copy link
Member

@jcouv jcouv Jun 15, 2021

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?

Copy link
Member Author

@cston cston Jun 15, 2021

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.

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member Author

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)
Copy link
Member

@jcouv jcouv Jun 15, 2021

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

Copy link
Member Author

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 () => { });
Copy link
Member

@jcouv jcouv Jun 15, 2021

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

Copy link
Member

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

Copy link
Member Author

@cston cston Jun 15, 2021

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))
Copy link
Member

@jcouv jcouv Jun 15, 2021

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

Copy link
Member

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 _));
Copy link
Member

@jcouv jcouv Jun 15, 2021

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

Copy link
Member

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

Copy link
Member Author

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.

@jcouv
Copy link
Member

jcouv commented Jun 15, 2021

    public BoundLambda BindForReturnTypeInference(NamedTypeSymbol delegateType)

I assume BindForReturnTypeInference is never called when the return type is explicit, so we never populate _returnInferenceCache in that case. Is that correct? If yes, is there an assertion we could include here?


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)

@jcouv
Copy link
Member

jcouv commented Jun 15, 2021

            SecurityWellKnownAttributeData securityData = wellKnownData.SecurityInformation;

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;
Copy link
Member

@jcouv jcouv Jun 15, 2021

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

Copy link
Member Author

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;
Copy link
Member

@jcouv jcouv Jun 15, 2021

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

Copy link
Member Author

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.

Copy link
Member

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) &&
Copy link
Member

@jcouv jcouv Jun 15, 2021

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

Copy link
Member Author

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))
Copy link
Member

@jcouv jcouv Jun 15, 2021

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

Copy link
Member Author

@cston cston Jun 15, 2021

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.

Copy link
Member

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
Copy link
Member

@jcouv jcouv Jun 15, 2021

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

Copy link
Member Author

@cston cston Jun 15, 2021

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));
}
Copy link
Member

@jcouv jcouv Jun 15, 2021

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

Copy link
Member Author

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;
Copy link
Member

@jcouv jcouv Jun 15, 2021

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

Copy link
Member

@jcouv jcouv left a 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);
Copy link
Member

@jaredpar jaredpar Jun 15, 2021

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

@cston
Copy link
Member Author

cston commented Jun 15, 2021

    public BoundLambda BindForReturnTypeInference(NamedTypeSymbol delegateType)

This method is still used when the return type is explicit. (It is the call to ReallyInferReturnType() below that avoids calculating the return type if explicit.) And we're binding the lambda body and caching the resulting BoundLambda rather than the just the return type here.

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?
Copy link
Member

@jcouv jcouv Jun 15, 2021

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

Copy link
Member

@jcouv jcouv left a 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))
Copy link
Member

@333fred 333fred Jun 17, 2021

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

Copy link
Member Author

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?
Copy link
Member

@333fred 333fred Jun 17, 2021

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

Copy link
Member Author

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;
Copy link
Member

@333fred 333fred Jun 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Syntax as ParenthesizedLambdaExpressionSyntax)?.ReturnType is not null

Nit: could be Syntax is ParenthesizedLambdaExpressionSyntax { ReturnType: not null } #Closed

}

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.
Copy link
Member

@333fred 333fred Jun 17, 2021

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;
Copy link
Member

@333fred 333fred Jun 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(int X, int Y) () => default

Conceptually, this feels similar to an override changing the names of a tuple. Do we want to consider a warning/error? #ByDesign

Copy link
Member Author

@cston cston Jun 19, 2021

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;
Copy link
Member

@333fred 333fred Jun 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment #ByDesign

@333fred
Copy link
Member

333fred commented Jun 17, 2021

Done review pass (commit 5)

@cston
Copy link
Member Author

cston commented Jun 19, 2021

@jcouv, @333fred, all feedback has been addressed, thanks.

}

return null;
return Binder.GetMethodGroupOrLambdaDelegateType(
Copy link
Member

@jcouv jcouv Jun 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetMethodGroupOrLambdaDelegateType

It looks like the other caller of GetMethodGroupOrLambdaDelegateType has some similar logic for handling void. Should we push this logic down into GetMethodGroupOrLambdaDelegateType? #Resolved

Copy link
Member Author

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.

Copy link
Member

@jcouv jcouv left a 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)

@cston cston merged commit 56c3369 into dotnet:features/lambdas Jun 21, 2021
@cston cston deleted the bind-type branch June 21, 2021 21:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants