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

Use lambda expression and method group signature in type inference #55786

Merged
merged 25 commits into from Aug 28, 2021

Conversation

cston
Copy link
Member

@cston cston commented Aug 21, 2021

Use lambda expression and method group signature in method type inference and best common type calculation.

Support implicit conversions of lambda expressions and method groups to object and interfaces of System.Delegate.

static T Identity<T>(T t) => t;

var m = Identity(Main);    // Action<string[]>
var a = new[] { () => 1 }; // Func<int>[]
object o = () => { };      // Action

The implementation adds a Signature property to UnboundLambda and BoundMethodGroup which represents the inferred "function type" of the lambda or method group - a signature although not tied to a particular delegate or Expression type.
The Signature is used in place of Type (which is null) in a few specific code paths - conversions, best common type, and method type inference - to support new conversions and inference from the "function type".

The signature is implemented as a FunctionTypeSymbol which is a new TypeSymbol kind. That is an implementation detail only, to simplify use in the code paths that are already expecting TypeSymbol. Other than those code paths, instances of FunctionTypeSymbol should not be observable.

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

@cston cston force-pushed the lambdas-type-inference branch 2 times, most recently from fa52250 to 7d295dc Compare August 23, 2021 00:15
@cston cston marked this pull request as ready for review August 24, 2021 17:35
@cston cston requested review from a team as code owners August 24, 2021 17:35
@cston
Copy link
Member Author

cston commented Aug 26, 2021

@dotnet/roslyn-compiler, please review.

{
if ((object?)_lazyFunctionType == FunctionTypeSymbol.Uninitialized)
{
var delegateType = _calculateDelegate(_binder!, _expression!);
Copy link
Member

@jcouv jcouv Aug 26, 2021

Choose a reason for hiding this comment

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

Should use an assertion for _expression here. (How do we know it's not null?)
I don't think _binder could be null, so suppression could be removed. #Closed

internal sealed class FunctionSignature
{
private readonly AssemblySymbol _assembly;
private readonly Binder? _binder;
Copy link
Member

@jcouv jcouv Aug 26, 2021

Choose a reason for hiding this comment

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

How can _binder be null? #Closed


internal FunctionSignature(Binder binder, Func<Binder, BoundExpression, NamedTypeSymbol?> calculateDelegate)
{
_assembly = binder.Compilation.Assembly;
Copy link
Member

@jcouv jcouv Aug 26, 2021

Choose a reason for hiding this comment

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

We're already storing the binder, seems like we could do without storing the assembly too. #Closed

internal void SetExpression(BoundExpression expression)
{
Debug.Assert((object?)_lazyFunctionType == FunctionTypeSymbol.Uninitialized);
Debug.Assert(_expression is null);
Copy link
Member

@jcouv jcouv Aug 26, 2021

Choose a reason for hiding this comment

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

nit: The expression is either a BoundMethodGroup or an UnboundLambda. Let's add an assertion for clarity. #Closed

{
internal static readonly FunctionTypeSymbol Uninitialized = new FunctionTypeSymbol();

private const SymbolKind s_SymbolKind = SymbolKind.FunctionPointerType + 1;
Copy link
Member

@jcouv jcouv Aug 26, 2021

Choose a reason for hiding this comment

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

Consider using negative values for both enums (SymbolKind and TypeKind), so that we don't have to update them in the future. Also, I'd add comments in those enums to document the existence of internal entries. #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.

TypeKind has an underlying type of byte so that value cannot be negative unfortunately. I've changed both values to 255.

And rather than adding comments to the public enums, I've added an assert here that the values are not existing values in the enums. That won't catch clashes with other internal values but it seemed like a simple local solution.

Copy link
Member

Choose a reason for hiding this comment

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

Without a (non-doc) comment in the enum I'm afraid it will be a bit difficult to find what values are being used in the codebase.
Alternatively, we could add an internal type next to the public enum type and put those constants there (next to the enum, which is more centralized).

Copy link
Member Author

Choose a reason for hiding this comment

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

Moving these next to the public enums makes sense, thanks.

private const SymbolKind s_SymbolKind = SymbolKind.FunctionPointerType + 1;
private const TypeKind s_TypeKind = TypeKind.FunctionPointer + 1;

private readonly AssemblySymbol? _assembly;
Copy link
Member

@jcouv jcouv Aug 26, 2021

Choose a reason for hiding this comment

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

Can _assembly be null?
If not, then we also don't need conditional access on usage (line 70 below)

Oh, I see this is possible for Uninitialized... Maybe we should suppress the nullability warning for that instance (which is not supposed to be used) so that the default assumption fits the normal usage. #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.

I've left _assembly as nullable since it's possible some of these methods or properties may be hit for the Uninitialized instance - in the debugger for instance.

@@ -1966,6 +1967,7 @@
<!-- Type is not significant for this node type; always null -->
<Field Name="Type" Type="TypeSymbol?" Override="true" Null="always"/>
<Field Name="Data" Type="UnboundLambdaState" Null="disallow"/>
<Field Name="Signature" Type="FunctionSignature?" Null="allow"/>
Copy link
Contributor

@AlekseyTs AlekseyTs Aug 26, 2021

Choose a reason for hiding this comment

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

This change looks suspiciaous. I believe UnboundLambda node is shared across all attempts to bind/convert a lambda expression. Several of them can succeed and succeed differently. #Closed

Copy link
Member Author

@cston cston Aug 26, 2021

Choose a reason for hiding this comment

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

The inferred signature is based on the lambda expression only. The signature should be independent of any conversions to specific delegate types.

{
internal static readonly FunctionTypeSymbol Uninitialized = new FunctionTypeSymbol();

private const SymbolKind s_SymbolKind = SymbolKind.FunctionPointerType + 1;
Copy link
Member

@jcouv jcouv Aug 26, 2021

Choose a reason for hiding this comment

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

Consider adding an assertion in SymbolDisplay that such symbols never reach there.
Never mind. SymbolDisplay works on ISymbol and we can get one from a FunctionTypeSymbol (code throws unreachable below) #Closed

private readonly NamedTypeSymbol _delegateType;

private FunctionTypeSymbol()
{
Copy link
Member

@jcouv jcouv Aug 26, 2021

Choose a reason for hiding this comment

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

_assembly = null!;? #Closed


internal override string GetDebuggerDisplay()
{
return $"DelegateType: {_delegateType.ToDisplayString(s_debuggerDisplayFormat)}";
Copy link
Member

@jcouv jcouv Aug 26, 2021

Choose a reason for hiding this comment

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

_delegateType.GetDebuggerDisplay()?
Then s_debuggerDisplayFormat can remain private. #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.

_delegateType.GetDebuggerDisplay()?

I like it, thanks.

[DebuggerDisplay("{GetDebuggerDisplay(),nq}")]
internal sealed class FunctionTypeSymbol : TypeSymbol
{
internal static readonly FunctionTypeSymbol Uninitialized = new FunctionTypeSymbol();
Copy link
Member

@jcouv jcouv Aug 26, 2021

Choose a reason for hiding this comment

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

nit: Should we also use a singleton for the instance with null _delegateType? #Closed

Copy link
Member Author

@cston cston Aug 26, 2021

Choose a reason for hiding this comment

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

There are no instances of FunctionTypeSymbol with null _delegateType currently.

Were you suggesting using such a singleton instance rather than null for FunctionSignature._lazyFunctionType? If so, I think that would just change how callers of FunctionSignature.GetSignatureAsTypeSymbol() check for null.

Copy link
Member

Choose a reason for hiding this comment

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

I got confused by this code:

     private bool HasImplicitFunctionTypeConversion(FunctionTypeSymbol source, TypeSymbol destination, ref CompoundUseSiteInfo<AssemblySymbol> useSiteInfo)
        {
            if (source.GetInternalDelegateType() is null)
            {
                return false;
            }
...

But it's actually not possible for FunctionTypeSymbol.GetInternalDelegateType() to return null.

internal bool IsAssignableFromMulticastDelegate(TypeSymbol type)
{
var useSiteInfo = CompoundUseSiteInfo<AssemblySymbol>.Discarded;
return ClassifyImplicitConversionFromType(corLibrary.GetSpecialType(SpecialType.System_MulticastDelegate), type, ref useSiteInfo).Exists;
Copy link
Contributor

@AlekseyTs AlekseyTs Aug 27, 2021

Choose a reason for hiding this comment

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

corLibrary.GetSpecialType(SpecialType.System_MulticastDelegate)

If useSiteInfo becomes a parameter, we need to pick up info from this symbol as well, I think. #Closed

return IsValidFunctionTypeConversionTarget(destination, ref useSiteInfo);
}

internal bool IsValidFunctionTypeConversionTarget(TypeSymbol destination, ref CompoundUseSiteInfo<AssemblySymbol> useSiteInfo)
Copy link
Contributor

Choose a reason for hiding this comment

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

IsValidFunctionTypeConversionTarget

Echoing Julien's concern about the difference between the implementation and the spec.

/// Internal Symbol representing the inferred signature of
/// a lambda expression or method group.
/// </summary>
internal const SymbolKind FunctionType = (SymbolKind)255;
Copy link
Contributor

Choose a reason for hiding this comment

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

FunctionType

Cannot say I am happy about this approach, but cannot think of a "better" alternative at the moment.

@@ -8636,7 +8619,6 @@ static bool isCandidateUnique(ref MethodSymbol? method, MethodSymbol candidate)
if (wkDelegateType != WellKnownType.Unknown)
{
var delegateType = Compilation.GetWellKnownType(wkDelegateType);
delegateType.AddUseSiteInfo(ref useSiteInfo);
Copy link
Contributor

@AlekseyTs AlekseyTs Aug 27, 2021

Choose a reason for hiding this comment

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

delegateType.AddUseSiteInfo(ref useSiteInfo);

It is not obvious why we don't want to do this. #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.

The caller of GetMethodGroupOrLambdaDelegateType() is responsible for checking any use-site diagnostics on the return type. Added a comment.

functionType.GetValue() is null)
{
var discardedUseSiteInfo = CompoundUseSiteInfo<AssemblySymbol>.Discarded;
if (targetType.IsExpressionTree() ||
Copy link
Contributor

@AlekseyTs AlekseyTs Aug 27, 2021

Choose a reason for hiding this comment

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

targetType.IsExpressionTree()

This is an expression tree with a target delegate type. Correct? In this case how inferring type from lambda would help? #Closed

if (targetType.IsExpressionTree() ||
Conversions.IsValidFunctionTypeConversionTarget(targetType, ref discardedUseSiteInfo))
{
Error(diagnostics, ErrorCode.ERR_CannotInferDelegateType, syntax);
Copy link
Contributor

@AlekseyTs AlekseyTs Aug 27, 2021

Choose a reason for hiding this comment

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

Error(diagnostics, ErrorCode.ERR_CannotInferDelegateType, syntax);

Should reporting of this error be conditioned on a language version? #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 only hit this code path if FunctionType is { } which requires language version 10 or higher.

BoundMethodGroup methodGroup => GetMethodGroupDelegateType(methodGroup, ref useSiteInfo),
_ => throw ExceptionUtilities.UnexpectedValue(expr),
};
var delegateType = expr.GetInferredDelegateType(ref useSiteInfo);
Copy link
Member

@jcouv jcouv Aug 27, 2021

Choose a reason for hiding this comment

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

Consider adding assertion here (only BoundMethodGroup or UnboundLambda) #Closed

conversion = Conversions.ClassifyConversionFromExpression(expr, destination, ref useSiteInfo);
diagnostics.Add(syntax, useSiteInfo);
return CreateConversion(syntax, expr, conversion, isCast, conversionGroup, destination, diagnostics);
delegateType = Compilation.GetWellKnownType(WellKnownType.System_Linq_Expressions_Expression_T).Construct(delegateType);
Copy link
Contributor

@AlekseyTs AlekseyTs Aug 27, 2021

Choose a reason for hiding this comment

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

Compilation.GetWellKnownType(WellKnownType.System_Linq_Expressions_Expression_T)

Why do we assume there is a relationship between the two types? #Closed

Copy link
Member Author

@cston cston Aug 27, 2021

Choose a reason for hiding this comment

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

Added tests where Expression<T> is not derived from Expression.

{
return false;
}
var expressionType = compilation.GetWellKnownType(WellKnownType.System_Linq_Expressions_Expression);
Copy link
Contributor

@AlekseyTs AlekseyTs Aug 27, 2021

Choose a reason for hiding this comment

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

var expressionType = compilation.GetWellKnownType(WellKnownType.System_Linq_Expressions_Expression);

Helpers in TypeSymbolExtensions do not use well known types to check if a type is an Expression. Doing this here feels inconsistent. #Closed

return delegateType;
}

public static TypeSymbol? GetTypeOrSignature(this BoundExpression expr)
Copy link
Member

@jcouv jcouv Aug 27, 2021

Choose a reason for hiding this comment

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

GetTypeOrSignature

nit: maybe GetTypeOrFunctionType #Closed

@@ -273,6 +272,7 @@ private enum Dependency
}

var inferrer = new MethodTypeInferrer(
binder.Compilation,
Copy link
Contributor

@AlekseyTs AlekseyTs Aug 27, 2021

Choose a reason for hiding this comment

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

binder.Compilation

Taking dependency on entire compilation object feels like a big hammer. Is it possible to narrow the dependency? #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.

After modifying isExpressionType() to use TypeSymbolExtensions.IsGenericOrNonGenericExpressionType(), the only use of CSharpCompilation is the use of compilation.GetWellKnownType(WellKnownType.System_Linq_Expressions_Expression_T) to construct the inferred type. If needed, we could pass in that well-known type to MethodTypeInferrer rather than passing in the CSharpCompilation. For now, I'll leave as-is.

@jcouv
Copy link
Member

jcouv commented Aug 27, 2021

    var a2 = new[] { F1, F2 };

Consider mixing method group and lambda.
nit: Also consider mixing lambda and static lambda (static doesn't matter)


In reply to: 907373662


In reply to: 907373662 #Pending


Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/DelegateTypeTests.cs:3418 in 5a2911a. [](commit_id = 5a2911a, deletion_comment = False)

@jcouv
Copy link
Member

jcouv commented Aug 27, 2021

    public void LambdaReturn_01()

nit: In addition to arrays and lambda returns, consider testing ??. It's not relying on best common type if I recall, and it's not very useful, but may be worth covering: () => {} ?? M.
I assume it'll fail.


In reply to: 907374774 #Pending


Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/DelegateTypeTests.cs:3540 in 5a2911a. [](commit_id = 5a2911a, deletion_comment = False)

@jcouv
Copy link
Member

jcouv commented Aug 27, 2021

    }

Here's a few exploratory test ideas:

  • nullability: var x = () => {};, var x = M;, var x = new[] { () => { } };, T M<T>(T t) where T : System.Delegate? called with () => {} or M, then verifying inferred nullability
  • local functions: use a local function for method group
  • stackalloc instead of array creation
  • lambda or method group in various contexts: switch (M) { case Delegate: ... }, var (x, y) = M;, _ = M with { };, using/lock/await/interpolation hole/throw
  • best common type and method type inference between a function type and null or default #Pending

Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/DelegateTypeTests.cs:4717 in 5a2911a. [](commit_id = 5a2911a, deletion_comment = False)

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 20)

@AlekseyTs
Copy link
Contributor

Done with review pass (commit 20), tests are not looked at.

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 25)

Copy link
Contributor

@AlekseyTs AlekseyTs left a comment

Choose a reason for hiding this comment

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

LGTM (commit 25)

@cston cston merged commit 4b95718 into dotnet:main Aug 28, 2021
@ghost ghost added this to the Next milestone Aug 28, 2021
@cston cston deleted the lambdas-type-inference branch August 28, 2021 01:34
@cston cston modified the milestones: Next, 17.0.P4 Aug 28, 2021
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.

None yet

3 participants