Fix double execution of Target expressions in null conditionals#24765
Fix double execution of Target expressions in null conditionals#24765SeeminglyScience wants to merge 4 commits intomasterfrom
Target expressions in null conditionals#24765Conversation
|
/azp run |
|
Azure Pipelines successfully started running 5 pipeline(s). |
There was a problem hiding this comment.
@SeeminglyScience Thanks for fixing this! Sorry for the delay in review.
| : DynamicExpression.Dynamic(PSGetDynamicMemberBinder.Get(_memberFunctionType, memberExpressionAst.Static), typeof(object), target, Compile(memberExpressionAst.Member)); | ||
|
|
||
| return memberExpressionAst.NullConditional ? GetNullConditionalWrappedExpression(target, memberAccessExpr) : memberAccessExpr; | ||
| return MaybeWrapInNullConditionalExpression( |
There was a problem hiding this comment.
The way you abstract the code with MaybeWrapInNullConditionalExpression feels complex and not very readable.
Take this particular call site as an example, how about abstract it in the way shown with the code below? With this approach, we only need to make minor changes to the existing code
- add
var targetToUse =section - replace
targetwithtargetToUsewhen constructing the member-access expression - change the call to
GetNullConditionalWrappedExpressionslightly
var target = CompileExpressionOperand(memberExpressionAst.Expression);
// **** Use a temp variable to represent the target for null-conditional ****
var targetToUse = memberExpressionAst.NullConditional
? Expression.Parameter(target.Type)
: target;
// If the ?. operator is used for null conditional check, add the null conditional expression.
// **** Generate the member-access expression with `targetToUse` ****
var memberNameAst = memberExpressionAst.Member as StringConstantExpressionAst;
Expression memberAccessExpr = memberNameAst != null
? DynamicExpression.Dynamic(PSGetMemberBinder.Get(memberNameAst.Value, _memberFunctionType, memberExpressionAst.Static), typeof(object), targetToUse)
: DynamicExpression.Dynamic(PSGetDynamicMemberBinder.Get(_memberFunctionType, memberExpressionAst.Static), typeof(object), targetToUse, Compile(memberExpressionAst.Member));
return memberExpressionAst.NullConditional
? GetNullConditionalWrappedExpression((ParameterExpression)targetToUse, target, memberAccessExpr)
: memberAccessExpr;And change GetNullConditionalWrappedExpression to the following:
private static Expression GetNullConditionalWrappedExpression(ParameterExpression temp, Expression targetExpr, Expression memberAccessExpression)
{
return Expression.Block(
typeof(object),
[temp],
[
Expression.Assign(temp, targetExpr),
Expression.Condition(
Expression.Call(
CachedReflectionInfo.LanguagePrimitives_IsNull,
temp.Cast(typeof(object))),
ExpressionCache.NullConstant,
memberAccessExpression),
]);
}There was a problem hiding this comment.
Definitely agree on the complexity 💯
That suggestion might work! I'll give it a go when I get a minute. Thanks Dongbo!
|
/azp run |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
|
|
/azp run |
|
Azure Pipelines successfully started running 1 pipeline(s). |
Now uses a temp variable created in the caller instead of a delegate. Thanks to @daxian-dbw for the pattern recommendation!
|
@SeeminglyScience Some tests are failing in CI, can you please take a look? |
|
This pull request has been automatically marked as Review Needed because it has been there has not been any activity for 7 days. |
Fixes #24716
PR Summary
Null conditional expressions (both index and member access) currently evaluate the "target" expression twice. This change saves the "target" result to a temporary variable to compare against.
This change is similar to #12667 which was done to fix null coalesce expressions.
Disassembly
As compiler changes can be challenging to review, I've included some disassembly of compiled scriptblocks before and after the changes. The module uses reflection to get the delegate compiled internally for the end block, and retrieves the associated LINQ expression tree. That expression tree is then interpreted into representative C#
These are manually formatted and with some of the irrelevant parts removed, full disassembly can be viewed here.
Member Expressions
Before Changes (click to expand)
After - target not strongly typed (click to expand)
```powershell [int] $i = 0 $null = ($i++)?.ToString() ```After - target strongly typed as valuetype (click to expand)
After - target strongly typed as ref type (click to expand)
Index Expressions
Before Changes (click to expand)
After - target not strongly typed (click to expand)
After - target strongly typed as valuetype (click to expand)
After - target strongly typed as ref type (click to expand)
PR Context
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.- [ ] Issue filed:
(which runs in a different PS Host).