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
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ public QueryUnboundLambdaState(Binder binder, RangeVariableMap rangeVariableMap,
public override SyntaxList<AttributeListSyntax> ParameterAttributes(int index) => default;
public override bool HasNames { get { return true; } }
public override bool HasSignature { get { return true; } }

public override bool HasExplicitReturnType(out RefKind refKind, out TypeWithAnnotations returnType)
{
refKind = default;
returnType = default;
return false;
}

public override bool HasExplicitlyTypedParameterList { get { return false; } }
public override int ParameterCount { get { return _parameters.Length; } }
public override bool IsAsync { get { return false; } }
Expand Down
38 changes: 32 additions & 6 deletions src/Compilers/CSharp/Portable/Binder/Binder_Lambda.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,12 @@ private UnboundLambda AnalyzeAnonymousFunction(
Debug.Assert(syntax != null);
Debug.Assert(syntax.IsAnonymousFunction());

var names = default(ImmutableArray<string>);
var refKinds = default(ImmutableArray<RefKind>);
var types = default(ImmutableArray<TypeWithAnnotations>);
var attributes = default(ImmutableArray<SyntaxList<AttributeListSyntax>>);
ImmutableArray<string> names = default;
ImmutableArray<RefKind> refKinds = default;
ImmutableArray<TypeWithAnnotations> types = default;
RefKind returnRefKind = RefKind.None;
TypeWithAnnotations returnType = default;
ImmutableArray<SyntaxList<AttributeListSyntax>> parameterAttributes = default;

var namesBuilder = ArrayBuilder<string>.GetInstance();
ImmutableArray<bool> discardsOpt = default;
Expand All @@ -67,6 +69,10 @@ private UnboundLambda AnalyzeAnonymousFunction(
// (x, y) => ...
hasSignature = true;
var paren = (ParenthesizedLambdaExpressionSyntax)syntax;
if (paren.ReturnType is { } returnTypeSyntax)
{
(returnRefKind, returnType) = BindExplicitLambdaReturnType(returnTypeSyntax, diagnostics);
}
parameterSyntaxList = paren.ParameterList.Parameters;
CheckParenthesizedLambdaParameters(parameterSyntaxList.Value, diagnostics);
break;
Expand Down Expand Up @@ -187,7 +193,7 @@ private UnboundLambda AnalyzeAnonymousFunction(

if (attributesBuilder.Any(a => a.Count > 0))
{
attributes = attributesBuilder.ToImmutable();
parameterAttributes = attributesBuilder.ToImmutable();
}

typesBuilder.Free();
Expand All @@ -202,7 +208,7 @@ private UnboundLambda AnalyzeAnonymousFunction(

namesBuilder.Free();

return new UnboundLambda(syntax, this, diagnostics.AccumulatesDependencies, attributes, refKinds, types, names, discardsOpt, isAsync, isStatic);
return new UnboundLambda(syntax, this, diagnostics.AccumulatesDependencies, returnRefKind, returnType, parameterAttributes, refKinds, types, names, discardsOpt, isAsync, isStatic);

static ImmutableArray<bool> computeDiscards(SeparatedSyntaxList<ParameterSyntax> parameters, int underscoresCount)
{
Expand Down Expand Up @@ -237,6 +243,26 @@ static void checkAttributes(AnonymousFunctionExpressionSyntax syntax, SyntaxList
}
}

private (RefKind, TypeWithAnnotations) BindExplicitLambdaReturnType(TypeSyntax syntax, BindingDiagnosticBag diagnostics)
{
MessageID.IDS_FeatureLambdaReturnType.CheckFeatureAvailability(diagnostics, syntax);

syntax = syntax.SkipRef(out RefKind refKind);
var returnType = BindType(syntax, diagnostics);
var type = returnType.Type;

if (returnType.IsStatic)
{
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.

{
diagnostics.Add(ErrorCode.ERR_MethodReturnCantBeRefAny, syntax.Location, type);
}

return (refKind, returnType);
}

private static void CheckParenthesizedLambdaParameters(
SeparatedSyntaxList<ParameterSyntax> parameterSyntaxList, BindingDiagnosticBag diagnostics)
{
Expand Down
13 changes: 11 additions & 2 deletions src/Compilers/CSharp/Portable/Binder/Binder_Statements.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1809,6 +1809,7 @@ internal BoundExpression GenerateConversionForAssignment(TypeSymbol targetType,
return CreateConversion(expression.Syntax, expression, conversion, isCast: false, conversionGroupOpt: null, targetType, diagnostics);
}

#nullable enable
internal void GenerateAnonymousFunctionConversionError(BindingDiagnosticBag diagnostics, SyntaxNode syntax,
UnboundLambda anonymousFunction, TypeSymbol targetType)
{
Expand Down Expand Up @@ -1868,9 +1869,13 @@ internal void GenerateAnonymousFunctionConversionError(BindingDiagnosticBag diag
return;
}

// 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

return;
}

var delegateType = targetType.GetDelegateType();
// At this point we know that we have either a delegate type or an expression type for the target.

// The target type is a valid delegate or expression tree type. Is there something wrong with the
// parameter list?
Expand All @@ -1896,6 +1901,9 @@ internal void GenerateAnonymousFunctionConversionError(BindingDiagnosticBag diag
return;
}

var delegateType = targetType.GetDelegateType();
Debug.Assert(delegateType is not null);

// There is a parameter list. Does it have the right number of elements?

if (reason == LambdaConversionResult.BadParameterCount)
Expand Down Expand Up @@ -2013,6 +2021,7 @@ internal void GenerateAnonymousFunctionConversionError(BindingDiagnosticBag diag
Debug.Assert(false, "Missing case in lambda conversion error reporting");
diagnostics.Add(ErrorCode.ERR_InternalError, syntax.Location);
}
#nullable disable

protected static void GenerateImplicitConversionError(BindingDiagnosticBag diagnostics, CSharpCompilation compilation, SyntaxNode syntax,
Conversion conversion, TypeSymbol sourceType, TypeSymbol targetType, ConstantValue sourceConstantValueOpt = null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1246,7 +1246,6 @@ source.Type is object &&
IsNumericType(source.Type) &&
IsConstantNumericZero(sourceConstantValue);
}
#nullable disable

private static LambdaConversionResult IsAnonymousFunctionCompatibleWithDelegate(UnboundLambda anonymousFunction, TypeSymbol type)
{
Expand All @@ -1266,6 +1265,15 @@ private static LambdaConversionResult IsAnonymousFunctionCompatibleWithDelegate(
return LambdaConversionResult.BadTargetType;
}

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.

{
return LambdaConversionResult.MismatchedReturnType;
}
}

var delegateParameters = invokeMethod.Parameters;

// SPEC: If F contains an anonymous-function-signature, then D and F have the same number of parameters.
Expand Down Expand Up @@ -1426,6 +1434,7 @@ private static bool HasAnonymousFunctionConversion(BoundExpression source, TypeS

return IsAnonymousFunctionCompatibleWithType((UnboundLambda)source, destination) == LambdaConversionResult.Success;
}
#nullable disable

internal Conversion ClassifyImplicitUserDefinedConversionForV6SwitchGoverningType(TypeSymbol sourceType, out TypeSymbol switchGoverningType, ref CompoundUseSiteInfo<AssemblySymbol> useSiteInfo)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using Microsoft.CodeAnalysis.CSharp.Symbols;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.CodeAnalysis.CSharp
{
internal enum LambdaConversionResult
Expand All @@ -16,6 +10,7 @@ internal enum LambdaConversionResult
BadTargetType,
BadParameterCount,
MissingSignatureWithOutParameter,
MismatchedReturnType,
MismatchedParameterType,
RefInImplicitlyTypedLambda,
StaticTypeInImplicitlyTypedLambda,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
#nullable disable

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using Microsoft.CodeAnalysis.CSharp.Symbols;
Expand All @@ -18,7 +17,7 @@ internal class WithLambdaParametersBinder : LocalScopeBinder
{
protected readonly LambdaSymbol lambdaSymbol;
protected readonly MultiDictionary<string, ParameterSymbol> parameterMap;
private SmallDictionary<string, ParameterSymbol> _definitionMap;
private readonly SmallDictionary<string, ParameterSymbol> _definitionMap;

public WithLambdaParametersBinder(LambdaSymbol lambdaSymbol, Binder enclosing)
: base(enclosing)
Expand All @@ -29,24 +28,17 @@ public WithLambdaParametersBinder(LambdaSymbol lambdaSymbol, Binder enclosing)
var parameters = lambdaSymbol.Parameters;
if (!parameters.IsDefaultOrEmpty)
{
recordDefinitions(parameters);
foreach (var parameter in lambdaSymbol.Parameters)
_definitionMap = new SmallDictionary<string, ParameterSymbol>();
foreach (var parameter in parameters)
{
if (!parameter.IsDiscard)
{
this.parameterMap.Add(parameter.Name, parameter);
}
}
}

void recordDefinitions(ImmutableArray<ParameterSymbol> definitions)
{
var declarationMap = _definitionMap ??= new SmallDictionary<string, ParameterSymbol>();
foreach (var s in definitions)
{
if (!s.IsDiscard && !declarationMap.ContainsKey(s.Name))
{
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 ;-)

{
_definitionMap.Add(name, parameter);
}
}
}
}
Expand Down
Loading