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
Further improve PSMethod to Delegate conversion #6851
Conversation
@@ -3486,6 +3443,64 @@ private static PSConverter<bool> CreateNumericToBoolConverter(Type fromType) | |||
return ConvertStringToEnum(sbResult.ToString(), resultType, recursion, originalValueToConvert, formatProvider, backupTable); | |||
} | |||
|
|||
private class ConvertPSMethodToDelegate |
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.
PSMethodDelegateConverter
might be a more "object-oriented" name for this class.
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.
Changed to PSMethodToDelegateConverter
.
return result; | ||
} | ||
|
||
internal Delegate Convert(object valueToConvert, |
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 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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".
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
while (signatureEnumerator.MoveNext()) | ||
{ | ||
var candidate = signatureEnumerator.Current.GetMethod("Invoke"); | ||
if (comparator.SignatureMatches(candidate.ReturnType, candidate.GetParameters())) | ||
index ++; |
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 would remove the whitespace between index
and ++
here.
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
} | ||
} | ||
} | ||
|
||
private class ConvertViaParseMethod |
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.
This class also has a verb-oriented name that should probably be changed.
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
convertable
-> convertible
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.
// 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment is amazing -- someone is going to be very grateful it exists one day.
return typeof(Func<PSNonBindableType>); | ||
} | ||
|
||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know you already renamed this, but something like methodTypes
seems clearer than allTypes
.
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.
|
||
struct PSEnum<T> { } | ||
|
||
class VOID { } |
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 we be explicit here with internal
?
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.
Yes, good catch. Done.
|
||
[Func[[E], [object]]] $f = $n.GetC | ||
$f.Invoke([E]::Week) | Should -BeExactly "Week" | ||
} |
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 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Much cleaner!
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 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very good point. I will add a placeholder type PSSpan<>
to represent a Span<>
type.
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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>
).
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.
Nice cleanup
// 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
).MatchesPSMethodProjectedType
fromPSMemberInfo.cs
to the typeSignatureComparator
inLanguagePrimitives.cs
, as it's closely related to the signature comparison. Also, renamed it toProjectedTypeMatchesTargetType
.-nopro
on a warm machine,measure.ps1.txt
is attached):PSEnum<T>
. We can directly use enum types when constructing the metadata typeFunc<..>
.MakeGenericMethod
with fake types likeGenericType0
,GenericType1
). This is because:Func<..>
metadata types. I left comments inGetMethodGroupType
method inPSMemberInfo.cs
to explain why that approach is better.PR Checklist
.h
,.cpp
,.cs
,.ps1
and.psm1
files 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