Compile += / -= to bind & handle event (de)registration directly#12930
Compile += / -= to bind & handle event (de)registration directly#12930vexx32 wants to merge 4 commits intoPowerShell:masterfrom
Conversation
|
@TravisEz13 is it intentional that test results do not appear to be publicly viewable anywhere? Azure Pipelines reports a test pass percentage, but all the links lead to a blank page, and the Tests tab is missing from those pipeline jobs. The few tests I could find failing in the log don't appear related at all, but I couldn't find all of them (and I'm not sure if there's a different way I need to be re-running failed jobs at the moment, too). |
|
@vexx32 There is a reference to test results |
|
@vexx32 There seems to be some change to Azure DevOps. The actual pipeline was not changed at all. The Azure DevOps app was just installed to allow integration with more GitHub bots. I suggest opening feedback in Azure DevOps, filing an issue with the link and mentioning me. I can follow up from there. |
|
@iSazonov yep! But I can only see the log files, which lack any kind of summary as to exactly which failed. The logs are thousands of lines long, surely we're not expected to painfully read through all of them for a few tiny Oh, hold on... okay on one of the result pages I can see it, but the Azure Pipelines pages themselves don't seem to have a proper summary of results from what I can see. |
|
@vexx32 I can definitely see the logs though, in an incognito window. Just the test tab is gone. |
|
@vexx32 we have moved to stage build for windows a long time ago... We moved the rest of the pipelines to stages... You may just be seeing the stage GUI and not be used to it. |
|
@TravisEz13 filed here: https://developercommunity.visualstudio.com/content/problem/1072124/individual-test-results-failures-not-shown-in-buil.html Didn't see any links pointing to a GH issues thing or anything, hopefully that's sufficient for now. Yeah, that's about what I'm seeing. Makes it a bit tricky since our test output is very condensed and at most I just see rows of I can see the stages and the logs individually, but... not super useful; tests tab just inexplicably gone 🙂
|
There was a problem hiding this comment.
Since you refactor the code you could move the Equals in separate if block and convert the switch to a switch expression.
There was a problem hiding this comment.
Please use s_ prefix for statics. Below too.
| private static readonly PSEventDelegateBinder _addInstanceBinder = new PSEventDelegateBinder( | |
| private static readonly PSEventDelegateBinder s_addInstanceBinder = new PSEventDelegateBinder( |
|
@vexx32 to find failure search for |
e6945fe to
2746cb4
Compare
There was a problem hiding this comment.
We think we should explicitly use InvariantCulture for formatting in the two places. It is our common pattern in Engine.
There was a problem hiding this comment.
AppendJoin() doesn't appear to have an overload that takes a format provider, but will do for AppendFormat().
There was a problem hiding this comment.
I think @iSazonov is referring to the string interpolation used in the Select delegate.
There was a problem hiding this comment.
I meant both - AppendFormat and the string interpolation.
There was a problem hiding this comment.
The same about InvariantCulture.
There was a problem hiding this comment.
| var invokeMethod = new MethodInformation( | |
| events[0].EventHandlerType.GetMethod("Invoke"), parametersToIgnore: 0).method; | |
| MethodInfo invokeMethod = events[0].EventHandlerType.GetMethod(nameof(Action.Invoke)); |
There was a problem hiding this comment.
| internal T GetDotNetEvent<T>(object obj, string methodName) where T : PSMemberInfo | |
| internal T GetDotNetEvent<T>(object obj, string eventName) where T : PSMemberInfo |
There was a problem hiding this comment.
| return GetDotNetEventImpl<T>(obj, methodName, predicate: null); | |
| return GetDotNetEventImpl<T>(obj, eventName, predicate: null); |
0b1a232 to
4408391
Compare
|
MacOS Failure is a known issue and not a new failure: #12935 |
|
This pull request has been automatically marked as Review Needed because it has been there has not been any activity for 7 days. |
|
@daxian-dbw friendly ping for review 😊 |
Sorry for the delay ... reviewing this one now. |
|
This pull request has been automatically marked as Review Needed because it has been there has not been any activity for 7 days. |
There was a problem hiding this comment.
This probably isn't needed -- it's probably simpler to let the GC just collect the whole string builder in one go when the method returns
There was a problem hiding this comment.
There's some interspersal of string building and formatting in here, along with some LINQ, all inside a non-threadsafe lazy logic block embedded in a property.
I think some possibilities to consider are:
- Move the string generation into its own method that just returns a value
- Use a
Lazy<string>likeEventDefinition => _eventDefinitionLazy.Value - Fully embrace either the string builder or the formatter, depending on whether we're trying to optimise for performance or readability
So for example:
private Lazy<string> _eventDefinitionLazy = new Lazy<string>(CreateEventDefinition);
internal string EventDefinition => _eventDefinitionLazy.Value;
private string CreateEventDefinition()
{
var invokeMethod = new MethodInformation(
this.events[0].EventHandlerType.GetMethod(nameof(Action.Invoke)),
parametersToIgnore: 0);
var sb = new StringBuilder()
.Append("event ")
.Append(ToStringCodeMethods.Type(events[0].EventHandlerType))
.Append(' ')
.Append(events[0].Name)
.Append('(');
ParameterInfo[] invocationParameters = invokeMethod.GetParameters();
if (invocationParameters.Length > 0)
{
ParameterInfo currParam = invocationParameters[0];
sb.Append(ToStringCodeMethods.Type(currParam.ParameterType))
.Append(' ')
.Append(currParam.Name);
for (int i = 1; i < invocationParameters.Length; i++)
{
currParam = invocationParameters[i];
sb.Append(", ")
.Append(ToStringCodeMethods.Type(currParam.ParameterType))
.Append(' ')
.Append(currParam.Name);
}
}
return sb.Append(')').ToString();
}There was a problem hiding this comment.
| foreach (var @event in eventTable.Values) | |
| { | |
| foreach (EventCacheEntry @event in eventTable.Values) | |
| { |
There was a problem hiding this comment.
| var expressionType = tokenKind switch | |
| ExpressionType expressionType = tokenKind switch |
There was a problem hiding this comment.
Any reason to make this static, if we just have to pass this back in?
There was a problem hiding this comment.
Not anymore! I wrote it this way initially as I wasn't sure if I'd need to callback to this from the binder or some such witchcraft, but that's definitely not required here.
There was a problem hiding this comment.
| // GetAddMethod().This lets us handle event delegates with += or -=, but we should only make this check | |
| // GetAddMethod(). This lets us handle event delegates with += or -=, but we should only make this check |
There was a problem hiding this comment.
This suggests that this will only work when events are given in the form:
$x.Event += $handlerBut in the comment below you give an alternate form:
$e = $x.Event
$e += $handlerShould we expect that to behave differently? If so, why? Is that consistent with other similar behaviours in PowerShell today?
There was a problem hiding this comment.
Hmm, that's a good point! I'm not sure, I guess I'd expect that that behaves the same. 🤔
I was mostly mimicking C#'s syntax, not sure saving the event to a var works there, but I guess I'd expect that works also.
There was a problem hiding this comment.
Looking back at this, the example you're referring to is more illustrating how the $object.EventName += $sb expression is compiled (it's saved to a temporary variable first) rather than what would literally work.
Do we want to allow this to work for PSEvent instances saved in a variable as well? My concern here was mainly that the $object.EventName += $b is likely to be the most common usage, and I didn't want to bog down every $var += $sb expression with a runtime check for $var -is [PSEvent] if I could avoid it.
There was a problem hiding this comment.
There was a problem hiding this comment.
Right, absolutely. I actually think you're right, but wanted to ask the question. (I referred to the compilation example because I knew it was a compilation example, but it was why I thought of it).
Events are strange second-class citizens in C# (I don't believe "event handler reference" is a meaningful concept for C#). But with that said, I'm not sure how many people we help by making them first-class in PowerShell (at the cost of things like runtime performance).
There was a problem hiding this comment.
👍 for how it's currently implemented. It's consistent with C# and also with how PSMethod's are handled. Like you can $method = $obj.ToString but you can't then do $method()
There was a problem hiding this comment.
What is the cast needed for here?
There was a problem hiding this comment.
I don't recall... I'll try it without, but I probably copied this pattern from elsewhere in the compiler.
There was a problem hiding this comment.
What is the cast needed for here?
More than likely it's because expressions don't implicitly box, the cast here will do that. Also matching expected types is super finicky (like a Expression.Block's return type, or a variable) which casting everything to object greatly simplifies.
That's why there's a ton of casting, but I don't know off the top of my head of getExpr would already be cast. (Side note, it's also why byref/likes will probably never be supported 🙁)
There was a problem hiding this comment.
Looking at how this is called, I think it does make sense the way it's currently written, but I just want to raise it for discussion.
A new possibility in one of the callers in this method now forces itself on all the other callers, leading to a number of calls where the caller must pass null in here. This seems to be common in the PowerShell codebase, but in my opinion leads to decohesion of methods, both from the perspective of the caller (which must now carefully select the appropriate subset of parameters) and from the perspective of the method itself (which must now carefully step between shared and scenario-specific logic).
In some respects this new parameter is a pseudo-boolean; it's either null or has a particular value that causes us to follow another path within the method.
That suggests to me that really we have two methods on our hands, with some shared logic best represented by breaking things up into smaller, composable methods.
There was a problem hiding this comment.
Definitely a good point. I can have a look at this and try to refactor things a bit.
There was a problem hiding this comment.
I think I have a pretty good way to do this. There's some duplication of code, but it separates the code paths much more neatly. I'll commit this change shortly; any additional suggestions you have to improve what I managed to get to are more than welcome! 😊
There was a problem hiding this comment.
| : PSObject.DotNetInstanceAdapter.GetPropertiesMethodsAndEvents(type, false); | |
| : PSObject.DotNetInstanceAdapter.GetPropertiesMethodsAndEvents(type, @static: false); |
4e008a8 to
9e092d7
Compare
|
This pull request has been automatically marked as Review Needed because it has been there has not been any activity for 7 days. |
|
@daxian-dbw Could you please review or make a conclusion about closing the PR? |
|
/rebase |
|
This pull request has been automatically marked as Review Needed because it has been there has not been any activity for 7 days. |
|
Any news on this? |
|
@vexx32 Is there any progress related to this? Do you know of any other issue/PR we can follow? |
|
I do not know if I personally have all that much time spare to look at this in detail but I will ping WG folks and to give it a look when they have a minute and get it on the radar, and put some labels on etc. It probably needs a rebase, at the least... I don't know if the WG folks will want to take it or not, but I think it's worth it myself. (/cc @SeeminglyScience) |
9e092d7 to
d08595f
Compare
|
it does look like a rebase is all that is needed (at least that's all i did, and the new tests were successful) |
This gives visibility into events so that users can target them. It also vastly simplifies the necessary handling in the compiler/binders as we can use a runtime conditional expression to determine if the target of the += / -= op is a PSEvent and maintain some sanity in the binders and compiler logic needed here. Completion logic also needed to be enhanced to provide completion for event names.
Adds a new binder to handle adding/removing event delegates with += / -= If the LHS is a MemberAccessExpressionAst, the compiler will compile +=/-= expressions into conditionals that first check if the LHS is a PSEvent member. If so, it attempts to register the RHS argument as a delegate for that event. If the LHS isn't PSEvent, falls back to the typical compound assignment.
We can split the code path into assignments to members, and assignments in other cases, avoiding the need to make some checks when the LHS is not a MemberExpressionAst. Reworked things a bit to use Lazy<string> for event definition. Also refactored logic to use StringBuilder completely per Rob's comments Co-authored-by: Robert Holt <rjmholt@gmail.com>
Ensure we check both instance and static members to ensure we add or remove the event correctly for both cases. Also added a tab completion test for event members, which we don't seem to have explicit tests for that I could find. As this completion is related to this PR, seemed appropriate to add tests for that behaviour here as well. Co-authored-by: Ilya <darpa@yandex.ru>
d08595f to
5121900
Compare
|
The Engine WG discussed this today (apologies for the wait). We worry that making this syntax easier (even if marginally) would imply a greater support for this operation than what we currently have (no closure created, handler must be invoked in a thread with a default runspace, potential runspace affinity issues, etc). Thank you for your hard work on this Rain, and again sorry for the very late response. |
|
📣 Hey @@vexx32, how did we do? We would love to hear your feedback with the link below! 🗣️ 🔗 https://aka.ms/PSRepoFeedback |



PR Summary
Permits the following syntax:
Changes
PSEventstatic and instance members of classes & enable tab completion for them.-=or+=only if the LHS is aMemberExpressionAst, combined with a runtime check for the LHS value beingPSEvent.PSInvokeMemberBinderwith that method name.PR Context
See #12926 for some additional context, but my thoughts are essentially: events are currently not discoverable and very annoying to work with in PowerShell, sometimes requiring reliance on implementation details.
I do not believe this can be considered a breaking change, since the only place it applies is specifically for event members, which were previously pretty much entirely inaccessible prior to this change.
Resolves #12926
With respect to tooling, there shouldn't be any breaking points, but event members will be offered in tab completion results with these changes, which may be considered unexpected in some circumstances.
PR Checklist
.h,.cpp,.cs,.ps1and.psm1files have the correct copyright headerWIP:or[ WIP ]to the beginning of the title (theWIPbot will keep its status check atPendingwhile the prefix is present) and remove the prefix when the PR is ready.