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
Conversation
fa52250
to
7d295dc
Compare
069d047
to
2572169
Compare
@dotnet/roslyn-compiler, please review. |
{ | ||
if ((object?)_lazyFunctionType == FunctionTypeSymbol.Uninitialized) | ||
{ | ||
var delegateType = _calculateDelegate(_binder!, _expression!); |
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 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; |
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.
How can _binder
be null? #Closed
|
||
internal FunctionSignature(Binder binder, Func<Binder, BoundExpression, NamedTypeSymbol?> calculateDelegate) | ||
{ | ||
_assembly = binder.Compilation.Assembly; |
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 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); |
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 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; |
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 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
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.
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.
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.
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).
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.
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; |
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.
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
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'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"/> |
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.
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; |
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 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() | ||
{ |
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.
_assembly = null!;
? #Closed
|
||
internal override string GetDebuggerDisplay() | ||
{ | ||
return $"DelegateType: {_delegateType.ToDisplayString(s_debuggerDisplayFormat)}"; |
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.
_delegateType.GetDebuggerDisplay()
?
Then s_debuggerDisplayFormat
can remain private. #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.
_delegateType.GetDebuggerDisplay()
?
I like it, thanks.
[DebuggerDisplay("{GetDebuggerDisplay(),nq}")] | ||
internal sealed class FunctionTypeSymbol : TypeSymbol | ||
{ | ||
internal static readonly FunctionTypeSymbol Uninitialized = new FunctionTypeSymbol(); |
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: Should we also use a singleton for the instance with null
_delegateType
? #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.
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
.
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 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; |
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.
return IsValidFunctionTypeConversionTarget(destination, ref useSiteInfo); | ||
} | ||
|
||
internal bool IsValidFunctionTypeConversionTarget(TypeSymbol destination, ref CompoundUseSiteInfo<AssemblySymbol> useSiteInfo) |
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.
/// Internal Symbol representing the inferred signature of | ||
/// a lambda expression or method group. | ||
/// </summary> | ||
internal const SymbolKind FunctionType = (SymbolKind)255; |
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.
@@ -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); |
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.
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() || |
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.
if (targetType.IsExpressionTree() || | ||
Conversions.IsValidFunctionTypeConversionTarget(targetType, ref discardedUseSiteInfo)) | ||
{ | ||
Error(diagnostics, ErrorCode.ERR_CannotInferDelegateType, syntax); |
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 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); |
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 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); |
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.
Added tests where Expression<T>
is not derived from Expression
.
{ | ||
return false; | ||
} | ||
var expressionType = compilation.GetWellKnownType(WellKnownType.System_Linq_Expressions_Expression); |
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.
return delegateType; | ||
} | ||
|
||
public static TypeSymbol? GetTypeOrSignature(this BoundExpression expr) |
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.
@@ -273,6 +272,7 @@ private enum Dependency | |||
} | |||
|
|||
var inferrer = new MethodTypeInferrer( | |||
binder.Compilation, |
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.
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.
Consider mixing method group and lambda. 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) |
nit: In addition to arrays and lambda returns, consider testing In reply to: 907374774 #Pending Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/DelegateTypeTests.cs:3540 in 5a2911a. [](commit_id = 5a2911a, deletion_comment = False) |
Here's a few exploratory test ideas:
Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/DelegateTypeTests.cs:4717 in 5a2911a. [](commit_id = 5a2911a, deletion_comment = False) |
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 20)
Done with review pass (commit 20), tests are not looked at. |
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 25)
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 (commit 25)
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 ofSystem.Delegate
.The implementation adds a
Signature
property toUnboundLambda
andBoundMethodGroup
which represents the inferred "function type" of the lambda or method group - a signature although not tied to a particular delegate orExpression
type.The
Signature
is used in place ofType
(which isnull
) 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 newTypeSymbol
kind. That is an implementation detail only, to simplify use in the code paths that are already expectingTypeSymbol
. Other than those code paths, instances ofFunctionTypeSymbol
should not be observable.Proposal: lambda-improvements.md
Test plan: #52192