Infer delegate type for method groups and anonymous functions#52448
Infer delegate type for method groups and anonymous functions#52448cston merged 18 commits intodotnet:features/lambdasfrom
Conversation
3338080 to
87f22bc
Compare
|
cc @halter73 |
| // (9,13): error CS8652: The feature 'inferred delegate type' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. | ||
| // if (a.E! != null) // 2 | ||
| Diagnostic(ErrorCode.ERR_BadBinaryOps, "a.E! != null").WithArguments("!=", "method group", "<null>").WithLocation(9, 13), | ||
| Diagnostic(ErrorCode.ERR_FeatureInPreview, "a.E").WithArguments("inferred delegate type").WithLocation(9, 13), |
There was a problem hiding this comment.
Please test with LangVer=preview (I assume the delegate is considered not-null, so warnings on lines 2 and 9 go away) #Closed
There was a problem hiding this comment.
DelegateTypeTests contains several specific tests for language version. For other tests, I chose to either use /langversion:9 or /langversion:preview but not both since both seemed unnecessary. Let me know if you'd prefer the other tests to use /langversion:preview.
| } | ||
|
|
||
| [Fact] | ||
| [ConditionalFact(typeof(IsRelease))] |
There was a problem hiding this comment.
What motivated the change? #Closed
There was a problem hiding this comment.
What motivated the change?
There is additional validation of lambdas in Binder.CreateAnonymousFunctionConversion(), and similar validation of method groups in Binder.CreateMethodGroupConversion(), that made these tests unnecessarily expensive in DEBUG. #Closed
There was a problem hiding this comment.
Consider leaving a comment explaining why we skip (it's just slow)
In reply to: 612782049 [](ancestors = 612782049)
| { | ||
| object o = () => { }; | ||
| System.ICloneable c = () => { }; | ||
| System.Delegate d = () => { }; |
There was a problem hiding this comment.
Should we also test MulticastDelegate per LDM discussion yesterday? #Closed
There was a problem hiding this comment.
PROTOTYPE for object and ICloneable (those should work too, right?)
In reply to: 612747777 [](ancestors = 612747777)
There was a problem hiding this comment.
Added failing test for System.MulticastDelegate and added items to the test plan for each (rather than adding PROTOTYPE comments). #Closed
| var comp = CreateCompilation(source); | ||
| comp.VerifyDiagnostics( | ||
| // (6,30): error CS0019: Operator '==' cannot be applied to operands of type '<null>' and 'lambda expression' | ||
| // (6,65): error CS8652: The feature 'inferred delegate type' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. |
There was a problem hiding this comment.
Could you also verify diagnostics with LangVer=preview? (also consider doing the same in TestTypelessTupleAndTupleOfDefaults above, and TestWithTypelessTuple below) #Closed
| yield return getData("static object F(int _1, object _2, int _3, object _4, int _5, object _6, int _7, object _8, int _9, object _10, int _11, object _12, int _13, object _14, int _15, object _16, int _17) => null;", "F", null, null); | ||
| yield return getData("T F<T>() => default;", "(new Program()).F<int>", "System.Func<System.Int32>", "Func`1"); | ||
|
|
||
| static object?[] getData(string methodDeclaration, string methodGroupExpression, string? expectedType, string? expectedName) => |
There was a problem hiding this comment.
It looks like methodGroupExpression is always "F". Consider removing that parameter and hard-coding in tests #Closed
There was a problem hiding this comment.
The method declaration and method group are related, and at the moment, the test and the test data are mostly independent. #Closed
| var delegateType = type.GetDelegateType(); | ||
| var delegateType = (type.SpecialType == SpecialType.System_Delegate) ? | ||
| // PROTOTYPE: We're resolving the method group multiple times in the code path for a single conversion. | ||
| _binder.GetMethodGroupDelegateType(methodGroup, diagnostics).Item1 : |
There was a problem hiding this comment.
Consider using tuple names.
Do we have a caller of GetMethodGroupDelegateType that consumes Item2? #Closed
There was a problem hiding this comment.
Do we have a caller of
GetMethodGroupDelegateTypethat consumesItem2?
Good catch, that value is unused. #Closed
| // C# preview features. | ||
| case MessageID.IDS_FeatureMixedDeclarationsAndExpressionsInDeconstruction: | ||
| case MessageID.IDS_FeatureLambdaAttributes: | ||
| case MessageID.IDS_FeatureInferredDelegateType: |
| foreach (var scope in new ExtensionMethodScopes(this)) | ||
| { | ||
| var methodGroup = MethodGroup.GetInstance(); | ||
| try |
There was a problem hiding this comment.
Do we need the try/finally? For comparison, BindExtensionMethod does without. #Closed
| d = t.F1<int>; | ||
| d = t.F1<T>; | ||
| d = t.F2<T, object>; | ||
| d = t.F2<object, T>; |
There was a problem hiding this comment.
Do we have a test similar to this (instance or extension method with type parameters converted to Delegate) but where type arguments are not provided on the method group? I assume then the method group will just be empty, is that right? #Closed
There was a problem hiding this comment.
There was a problem hiding this comment.
What was the resolution?
I've updated this test to include method groups without type arguments. It'll be in the next commit. #Closed
| return null; | ||
| } | ||
| var method = node.Methods.SingleOrDefault(); | ||
| if (node.SearchExtensionMethods) |
There was a problem hiding this comment.
Should we even look at extension methods if we've found exactly one instance method already?
It looks like this was discussed in LDM (I must have zoned out). From the spec:
Requiring a method group to contain a single method means that adding an overload (including an extension method overload) for a method that previously had no overloads is a breaking change if the original method was used as a method group with inferred type. #Closed
| void F() | ||
| { | ||
| Delegate d1 = F1; | ||
| Delegate d2 = this.F2; |
There was a problem hiding this comment.
It looks like base has special rules for binding method groups. Consider adding a test:
See BindInstanceMemberAccess (line 6290)
var searchExtensionMethodsIfNecessary = !leftIsBaseReference; #Closed
There was a problem hiding this comment.
Consider also testing with type as LHS: Delegate d3 = Program3.StaticMethod and Delegate d4 = Program4<int>.StaticMethod
Also test with a dynamic receiver (error)
In reply to: 612866844 [](ancestors = 612866844)
There was a problem hiding this comment.
Added ExtensionMethods_05() for base with an extension method; added tests for static methods with explicit type to GetMethodGroupData(); added DynamicReceiver() for dynamic test. #Closed
| { | ||
| // Must be a bona fide delegate type, not an expression tree type. | ||
| if (!destination.IsDelegateType()) | ||
| if (!(destination.IsDelegateType() || destination.SpecialType == SpecialType.System_Delegate)) |
There was a problem hiding this comment.
Should we also allow destination to be object, or ICloneable, or MulticastDelegate here? #Closed
There was a problem hiding this comment.
Should we also allow destination to be object, or ICloneable, or MulticastDelegate here?
Perhaps. Let's address later when handling those cases. #Closed
| constantValueOpt: ConstantValue.NotAvailable, | ||
| type: destination) | ||
| { WasCompilerGenerated = source.WasCompilerGenerated }; | ||
| if (destination.SpecialType == SpecialType.System_Delegate || destination.IsNonGenericExpressionType()) |
There was a problem hiding this comment.
Same here (object, ICloneable, etc) and below in createAnonymousFunctionConversion, CreateMethodGroupConversion, MethodGroupConversionHasErrors and other places in this PR. Consider extracting this check to a helper method, so that we don't forget to fix some places when adding support for those other types #Closed
There was a problem hiding this comment.
Let's address later when addressing implicit conversions to those base and interface types. #Closed
| var conversion = (resolution.IsEmpty || resolution.HasAnyErrors) ? | ||
| Conversion.NoConversion : | ||
| ToConversion(resolution.OverloadResolutionResult, resolution.MethodGroup, ((NamedTypeSymbol)destination).DelegateInvokeMethod.ParameterCount); | ||
| ToConversion(resolution.OverloadResolutionResult, resolution.MethodGroup, (destination.SpecialType == SpecialType.System_Delegate ? methodSymbol : ((NamedTypeSymbol)destination).DelegateInvokeMethod).ParameterCount); |
|
|
||
| return expr; | ||
|
|
||
| BoundConversion createAnonymousFunctionConversion(SyntaxNode syntax, UnboundLambda unboundLambda, Conversion conversion, bool isCast, ConversionGroup? conversionGroup, TypeSymbol destination, BindingDiagnosticBag diagnostics) |
There was a problem hiding this comment.
The local function uses this.Compilation. #Closed
| return true; | ||
| } | ||
| if (type.Arity == 1 && | ||
| type.MangleName) |
There was a problem hiding this comment.
nit: this is existing logic, but isn't it redundant with the Arity check? #Closed
There was a problem hiding this comment.
this is existing logic, but isn't it redundant with the Arity check?
This check will ensure we don't match to a generic type named Expression without the trailing `1, although that scenario is unlikely. Since this is an existing check, I'll leave it as is. #Closed
| else | ||
| { | ||
| lambdaSymbol = CreateLambdaSymbol(Binder.ContainingMemberOrLambda, returnType, cacheKey.ParameterTypes, cacheKey.ParameterRefKinds, refKind); | ||
| lambdaSymbol = CreateLambdaSymbol(Binder.ContainingMemberOrLambda!, returnType, cacheKey.ParameterTypes, cacheKey.ParameterRefKinds, refKind); |
There was a problem hiding this comment.
Added an assertion to UnboundLambda..ctor(). #Closed
| return true; | ||
| } | ||
|
|
||
| private BoundLambda ReallyBindNaturalType(TypeSymbol expressionType) |
There was a problem hiding this comment.
nit: consider calling the parameter "delegateType" to match caller and other similar method #Closed
There was a problem hiding this comment.
In this case, the type is not a (strongly-typed) delegate type though. It is either System.Delegate or System.Linq.Expressions.Expression. I realize the naming is inconsistent since callers refer to this as "delegateType" but I was reluctant to change the callers. The current names were a compromise although not ideal. #Closed
| }"); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Some test ideas:
- the spec calls out behavior when the lambda or method group involves a modreq or modopt, but I couldn't find tests for that. ("
modopt()ormodreq()in the method group signature are ignored in the corresponding delegate type.") - make
Action,Func, orDelegatemissing - have we covered this section of the spec: "if [...] any of the parameter types or return are not valid type arguments"?
- I'm not sure how to track this: we should remember to test delegate type inference once we support explicit return types. (I don't expect any problem though)
- You already have tests for Type and Converted types of the lambda and method group. We should do the same but also verifying inferred nullability.
varand invocation sections of the spec are intentionally out of scope for this PR, is that correct?- Do we have some tests that verify the emitted delegate type (as opposed to just the semantic model)? #Closed
There was a problem hiding this comment.
Thanks for the suggestions:
- Added tests for
modoptandmodreq. - Added tests for missing
System.ActionandSystem.Func; added missingSystem.Delegateto the test plan. - Added test for parameter types and return type that are not valid type arguments.
- No change.
- Added nullability to test plan.
- Correct,
varand directly invoking a lambda expression are out of scope. Included in test plan. - A number of tests report the delegate type from the compiled executable. #Closed
| comp.VerifyDiagnostics( | ||
| // (6,20): error CS0030: Cannot convert type 'method' to 'Delegate' | ||
| // object o = (System.Delegate)F; | ||
| Diagnostic(ErrorCode.ERR_NoExplicitConv, "(System.Delegate)F").WithArguments("method", "System.Delegate").WithLocation(6, 20)); |
| }"; | ||
| var comp = CreateCompilation(source, parseOptions: TestOptions.RegularPreview); | ||
| comp.VerifyDiagnostics(); | ||
| } |
There was a problem hiding this comment.
If we don't have tests for that already, can we check the natural type (on semantic model) in extension scenario? #Closed
There was a problem hiding this comment.
Tested type from the semantic model and at runtime. (Note, the semantic model currently does not expose the natural type. I've added an item to the test plan to handle that.) #Closed
| else | ||
| { | ||
| CompileAndVerify(comp, expectedOutput: expectedType); | ||
| } |
There was a problem hiding this comment.
Consider asserting TypeInfo here as well. #Resolved
There was a problem hiding this comment.
| else | ||
| { | ||
| comp.VerifyDiagnostics(); | ||
| } |
There was a problem hiding this comment.
Consider asserting TypeInfo here as well. #Resolved
There was a problem hiding this comment.
| if (m.IsStatic) continue; | ||
| break; | ||
| } | ||
| if (!updateCandidate(ref method, m)) |
There was a problem hiding this comment.
nit: the name and return value of this local function make this odd. Consider "if (!isCandidateUnique(ref candidate, m))`? #Closed
| {{ | ||
| void M() | ||
| {{ | ||
| System.Delegate d = {methodGroupExpression}; |
There was a problem hiding this comment.
Added runtime validation.
GetSymbolInfo() does not return the resolved extension method currently. I've added a PROTOTYPE comment for now.
| {{ | ||
| void M() | ||
| {{ | ||
| System.Delegate d = {methodGroupExpression}; |
| comp.VerifyDiagnostics( | ||
| // (14,15): error CS8915: The delegate type could not be inferred. | ||
| // d = p.F2; | ||
| Diagnostic(ErrorCode.ERR_CannotInferDelegateType, "F2").WithLocation(14, 15)); |
There was a problem hiding this comment.
This isn't making sense to me: why can't this be inferred to Action? #Closed
There was a problem hiding this comment.
why can't this be inferred to
Action?
Because there are two overloads with distinct signatures: Program.F2(int x) and E.F2(this object x). #Closed
There was a problem hiding this comment.
| if (!m.IsStatic) continue; | ||
| break; | ||
| case { WasCompilerGenerated: false }: | ||
| if (m.IsStatic) continue; |
There was a problem hiding this comment.
I need help understanding this (why distinguish on whether the receiver is compiler-generated or not). Let's chat later this afternoon. #Closed
There was a problem hiding this comment.
Changed to use explicit case BoundThisReference { ... }: for clarity based on our conversation. #Closed
| } | ||
| }"; | ||
|
|
||
| var expectedOutput = @"M(Action<string> a)"; |
There was a problem hiding this comment.
This should end up being M<T>(T t) in a follow up, right? #Closed
There was a problem hiding this comment.
Yes, this test and the following test will change. #Closed
| } | ||
|
|
||
| [Fact] | ||
| public void NullableAnalysis_01() |
There was a problem hiding this comment.
I'm not sure what these are actually testing? #Closed
There was a problem hiding this comment.
Added comments. #Closed
|
Done review pass (commit 15) |
| { | ||
| .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { ret } | ||
| }"; | ||
| var refA = CompileIL(sourceA, prependDefaultHeader: false, autoInherit: false); |
There was a problem hiding this comment.
nit: Is it necessary to use IL to test those types are missing? (also in test below). I'd have though MakeMemberMissing or a C# implementation could do.
It would be good to also test Expression e = () => {}; with missing Expression1` #Closed
There was a problem hiding this comment.
The IL is simple enough for these missing types so that seemed easiest.
Added a test for Expression<TDelegate>.
|
|
||
| var comp = CreateEmptyCompilation(sourceB, new[] { refA }, parseOptions: TestOptions.RegularPreview); | ||
| comp.VerifyDiagnostics( | ||
| // (9,13): error CS0648: 'Action<T>' is a type not supported by the language |
There was a problem hiding this comment.
Seems like we're getting a duplicate error. Is that expected? #Closed
There was a problem hiding this comment.
Yes, we're currently calling GetMethodGroupDelegateType() twice in the code path that generates the conversion. There are PROTOTYPE comments to avoid the duplicate calls. #Closed
| Diagnostic(ErrorCode.ERR_FeatureInPreview, "() => { }").WithArguments("inferred delegate type").WithLocation(8, 13)); | ||
|
|
||
| CompileAndVerify(source, parseOptions: TestOptions.RegularPreview, expectedOutput: | ||
| @"C.M |
There was a problem hiding this comment.
It would be good to comment on behavior change. Should document the break in compiler doc. #Closed
There was a problem hiding this comment.
The behavior has not changed yet. I will update the breaking change document at the same time. #Closed
There was a problem hiding this comment.
You're right, thanks. I've added a comment and updated the breaking change document. #Closed
jcouv
left a comment
There was a problem hiding this comment.
Done with review pass (iteration 16)
| return LambdaConversionResult.ExpressionTreeFromAnonymousMethod; | ||
| } | ||
|
|
||
| if (delegateType is null) |
There was a problem hiding this comment.
nit: Consider if (type.Arity == 0) ... if that is more directly what is intended, and assert that delegateType is not null otherwise. #ByDesign
There was a problem hiding this comment.
I'll leave as is, since delegateType here means the type argument to Expression<TDelegate> which is null for non-generic Expression.
jcouv
left a comment
There was a problem hiding this comment.
LGTM Thanks (iteration 17) with a question and a nit to consider.
333fred
left a comment
There was a problem hiding this comment.
LGTM (commit 17). A couple of test followup comments left, but they should be quick to address.
It looks like you added tests for GetSymbolInfo on method groups, but not lambdas. Could you show what's the current behavior and file an issue if needed? Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/DelegateTypeTests.cs:766 in b147208. [](commit_id = b147208, deletion_comment = False) |
|
🔥🔥🔥 |
Infer delegate type for method groups and anonymous functions.
The inferred delegate type is used to allow implicit conversion of method groups and lambdas to
System.Delegate.The inferred delegate type is ignored in other scenarios currently, so method groups and lambdas cannot be used as
varinitializers, nor implicitly converted toobjectwith this change.Proposal: lambda-improvements.md#natural-delegate-type
Test plan: #52192