Further improve PSMethod to Delegate conversion#6851
Further improve PSMethod to Delegate conversion#6851daxian-dbw merged 6 commits intoPowerShell:masterfrom
Conversation
| return ConvertStringToEnum(sbResult.ToString(), resultType, recursion, originalValueToConvert, formatProvider, backupTable); | ||
| } | ||
|
|
||
| private class ConvertPSMethodToDelegate |
There was a problem hiding this comment.
PSMethodDelegateConverter might be a more "object-oriented" name for this class.
There was a problem hiding this comment.
Changed to PSMethodToDelegateConverter.
| return result; | ||
| } | ||
|
|
||
| internal Delegate Convert(object valueToConvert, |
There was a problem hiding this comment.
I've only ever seen docs refer to delegate. Is this different? There seems to be a general preference for using the type aliases like delegate and string in code I've read both here and in documentation.
There was a problem hiding this comment.
delegate is a keyword for you to declare a delegate type, either named or anonymous. Unlike string, it cannot be used as a type, for example var a = typeof(delegate) results in a compilation error.
There was a problem hiding this comment.
Ah I see! I tried to find documentation on it, but Google doesn't do case-sensitivity well
| if (result == null) | ||
| { | ||
| var converter = new ConvertPSMethodToDelegate(matchIndex); | ||
| Threading.Interlocked.CompareExchange(ref s_converterCache[matchIndex], converter, null); |
There was a problem hiding this comment.
Might be worth adding a comment here explaining what's going on (reading up on Threading.Interlocked.CompareExchange takes a while). Just something like "if the cache entry is null, generate a new one and replace it".
| { | ||
| var candidate = signatureEnumerator.Current.GetMethod("Invoke"); | ||
| if (comparator.SignatureMatches(candidate.ReturnType, candidate.GetParameters())) | ||
| index ++; |
There was a problem hiding this comment.
I would remove the whitespace between index and ++ here.
| } | ||
| } | ||
|
|
||
| private class ConvertViaParseMethod |
There was a problem hiding this comment.
This class also has a verb-oriented name that should probably be changed.
There was a problem hiding this comment.
Rename a bunch of existing names will make the PR harder to review. I would prefer another PR if we want to change the names.
There was a problem hiding this comment.
Yes, agreed. Thought it was useful to express here on the basis of my other comment about a class name.
| { | ||
| if (methodInfo.DeclaringType.IsGenericTypeDefinition) | ||
| { | ||
| // If the method is from a generic type definition, consider it not convertable |
There was a problem hiding this comment.
convertable -> convertible
| // However, we don't yet handle generic methods in PSMethod-to-Delegate conversion, so for now, we | ||
| // don't produce the metadata type that represent the signature of a generic method. | ||
| // | ||
| // Say one day we want to support generic method in PSMethod-to-Delegate conversion and need to produce |
There was a problem hiding this comment.
This comment is amazing -- someone is going to be very grateful it exists one day.
|
|
||
| var res = new Type[parameterInfos.Length + 1]; | ||
| for (int i = 0; i < res.Length - 1; i++) | ||
| var allTypes = new Type[parameterInfos.Length + 1]; |
There was a problem hiding this comment.
I know you already renamed this, but something like methodTypes seems clearer than allTypes.
|
|
||
| struct PSEnum<T> { } | ||
|
|
||
| class VOID { } |
There was a problem hiding this comment.
Should we be explicit here with internal?
There was a problem hiding this comment.
Yes, good catch. Done.
|
|
||
| [Func[[E], [object]]] $f = $n.GetC | ||
| $f.Invoke([E]::Week) | Should -BeExactly "Week" | ||
| } |
There was a problem hiding this comment.
We should also include the following scenarios in tests to ensure we know current behaviour and detect regressions:
- Type constraint applied to an argument
[object]$var = "Hi"; $cls.TestMethod($var)$cls.TestMethod([object]"Hi")[Super]$sub = [Sub]::new(); $cls.TestMethodWithSuperAndSubOverloads($sub)$sub = [Sub]::new(); $cls.TestMethodWithSuperAndSubOverloads([Super]$sub)[string]$fileName = [System.IO.FileInfo]::new("myFile"); $cls.TestMethodWithFileInfoAndStringOverloads($fileName)$file = [System.IO.FileInfo]::new("myFile"); $cls.TestMethodWithFileInfoAndStringOverloads([string]$file)
class Cls
{
[string] TestMethod([string] $str) { return "String" }
[string] TestMethod([object] $obj) { return "Object" }
[string] TestMethodWithSuperAndSubOverloads([Super] $sup) { return "Super" }
[string] TestMethodWithSuperAndSubOverloads([Sub] $sub) { return "Sub" }
[string] TestMethodWithFileInfoAndStringOverloads([string] $str) { return "FileString" }
[string] TestMethodWithFileInfoAndStringOverloads([System.IO.FileInfo] $fileInfo) { return "FileInfo" }
}
$cls = [Cls]::new()- Sub-class which matches two or more method signatures, but none exactly
class Animal { }
class Cat : Animal { }
# If the order of method definition is what affects the method resolution using the current
# algorithm, we should have another test with the same methods in a different order on a different
# class to demonstrate that
class TestClass
{
[string] TestMethod([object] $obj) { return "Object" }
[string] TestMethod([Animal] $animal) { return "Animal" }
}- Interfaces
- Interface as parameter type in signature, object implements interface
- Overload of method for multiple interfaces, object implements both interfaces
- Implicit coercions
- I can't remember if we allow coercions in methods, but if so we must test how coercion works when
there are multiple overloads we could coerce to.
- I can't remember if we allow coercions in methods, but if so we must test how coercion works when
- Maybe some scenarios where the method binding should fail, so we cover the failure mode code paths
- No overload that matches
- Generic methods
There was a problem hiding this comment.
It looks to me your focus is on testing the overload resolution when calling a method, instead of PSMethod-to-Delegate conversion. This change doesn't touch the overload resolution for method invocation.
Note that, even if we will consider a more sophisticated resolution for this PSMethod-to-Delegate conversion feature, we probably won't be able to directly use the existing overload resolution code, because, for overload resolution of method invocation, powershell considers converting the argument to the target type, however, for this feature we cannot convert any source type (parameter or return) to the target type.
I do think the fail code path should be tested too.
|
Nice! |
|
@rjmholt Thanks for your review! |
| // In this way, '$metadataType.ContainsGenericParameters' returns 'True', indicating it represents a generic method. | ||
| // And also, given a generic argument type from `$metadataType.GetGenericArguments()`, it's easy to tell if it's a | ||
| // generic parameter (for example, 'T') based on the property 'IsGenericParameter'. | ||
| // Moreover, it's also easy to get constraints of the generic parameter, via 'GetGenericParameterConstraints()' |
There was a problem hiding this comment.
We may run into problems with representing methods containing Span<*> parameters.
Span cannot be a generic parameter. That is why I had all the convoluted PSGenericType stuff IIRC.
And since span is getting more and more usage, it would be nice if we have a story around it.
Sample below:
class A{ public static void Foo(Span<char> x){}}
delegate void SpanAFunc(Span<char> a);[SpanAFunc] $x = [A]::Foo;
There was a problem hiding this comment.
But it is probably the right thing to special case Span anyway, since it is an exceptional type.
There was a problem hiding this comment.
Very good point. I will add a placeholder type PSSpan<> to represent a Span<> type.
There was a problem hiding this comment.
In fact, it's not just types like Span<char> or ReadOnlySpan<int>, but a general problem for all ref-like types, such as public ref struct Foo { private Span<int> _pointer }. I don't have a generic solution for the ref-lik type at this moment, but at least we should handle Span<T> and ReadOnlySpan<T>. I have updated #5618 to track this work.
Interestingly, as typeof(Func<,>).MakeGenericType(new Type[] { typeof(Span<int>), typeof(int) }) in C# (7.2 language) throws TypeLoadException as expected, [Func`2].MakeGenericType(@([System.Span[int]], [int])) runs successfully in powershell 6.1.0-preview.2 and the build from latest master. Scratch that, it does fail in master branch now.
Good news is that we catch TypeLoadException when constructing the metadata type Func<>, so at least a method with ref-like type parameter or return type won't crash PowerShell even if MakeGenericType fails -- it will only mark the method as NonBindable and thus fails any conversion.
There was a problem hiding this comment.
I will merge this PR as is, and submit another PR later to address the ref-like types (or at least the Span<T> and ReadOnlySpan<T>).
| // In this way, '$metadataType.ContainsGenericParameters' returns 'True', indicating it represents a generic method. | ||
| // And also, given a generic argument type from `$metadataType.GetGenericArguments()`, it's easy to tell if it's a | ||
| // generic parameter (for example, 'T') based on the property 'IsGenericParameter'. | ||
| // Moreover, it's also easy to get constraints of the generic parameter, via 'GetGenericParameterConstraints()' |
There was a problem hiding this comment.
We may run into problems with representing methods containing Span<*> parameters.
Span cannot be a generic parameter. That is why I had all the convoluted PSGenericType stuff IIRC.
And since span is getting more and more usage, it would be nice if we have a story around it.
Sample below:
class A{ public static void Foo(Span<char> x){}}
delegate void SpanAFunc(Span<char> a);[SpanAFunc] $x = [A]::Foo;
| // In this way, '$metadataType.ContainsGenericParameters' returns 'True', indicating it represents a generic method. | ||
| // And also, given a generic argument type from `$metadataType.GetGenericArguments()`, it's easy to tell if it's a | ||
| // generic parameter (for example, 'T') based on the property 'IsGenericParameter'. | ||
| // Moreover, it's also easy to get constraints of the generic parameter, via 'GetGenericParameterConstraints()' |
There was a problem hiding this comment.
But it is probably the right thing to special case Span anyway, since it is an exceptional type.
|
The reminding CodeFactor issues are from the existing code, not changes from this PR. |
PR Summary
Fix partially #5618
Refactor code to make it easier to maintain and a little faster. Changes are as follows:
PSMethod<..>are generated based on the array of method overloads inMethodCacheEntry.MethodInformationStructures, in the exact same order. So inLanguagePrimitive.ConvertViaParseMethod, when we try to figure out if there is a match using the metadata signatures inPSMethod<..>, we can get the index of the matching signature, and the same index should locate the matching metod inMethodCacheEntry.MethodInformationStructures. Therefore, we don't need to compare signatures again in the actual conversion method, and instead, we can just leverage the index we found when figuring out conversion inConvertViaParseMethod.GetMethod("Invoke")and the subsequent signature comparisons in the final conversion method.PSMethod<..>inConvertViaParseMethod, we can just use the generaic argument types of eachFunc<..>metadata type, instead of callingGetMethod("Invoke")and thenGetParameters(). This makes the code for comparing signatures simpler (the typeSignatureComparator).MatchesPSMethodProjectedTypefromPSMemberInfo.csto the typeSignatureComparatorinLanguagePrimitives.cs, as it's closely related to the signature comparison. Also, renamed it toProjectedTypeMatchesTargetType.-noproon a warm machine,measure.ps1.txtis attached):PSEnum<T>. We can directly use enum types when constructing the metadata typeFunc<..>.MakeGenericMethodwith fake types likeGenericType0,GenericType1). This is because:Func<..>metadata types. I left comments inGetMethodGroupTypemethod inPSMemberInfo.csto explain why that approach is better.PR Checklist
.h,.cpp,.cs,.ps1and.psm1files have the correct copyright headerWIP:to the beginning of the title and remove the prefix when the PR is ready.[feature]if the change is significant or affects feature tests