Implement Null Coalescing and Null Coalescing assignment operators (#10636)

This commit is contained in:
Aditya Patwardhan 2019-10-17 10:21:24 -07:00 committed by Dongbo Wang
parent cb66974b25
commit 425bc36a6f
7 changed files with 404 additions and 16 deletions

View file

@ -120,7 +120,10 @@ namespace System.Management.Automation
description: "New formatting for ErrorRecord"),
new ExperimentalFeature(
name: "PSUpdatesNotification",
description: "Print notification message when new releases are available")
description: "Print notification message when new releases are available"),
new ExperimentalFeature(
name: "PSCoalescingOperators",
description: "Support the null coalescing operator and null coalescing assignment operator in PowerShell language")
};
EngineExperimentalFeatures = new ReadOnlyCollection<ExperimentalFeature>(engineFeatures);

View file

@ -221,6 +221,8 @@ namespace System.Management.Automation.Language
internal static readonly MethodInfo LanguagePrimitives_GetInvalidCastMessages =
typeof(LanguagePrimitives).GetMethod(nameof(LanguagePrimitives.GetInvalidCastMessages), staticFlags);
internal static readonly MethodInfo LanguagePrimitives_IsNullLike =
typeof(LanguagePrimitives).GetMethod(nameof(LanguagePrimitives.IsNullLike), staticPublicFlags);
internal static readonly MethodInfo LanguagePrimitives_ThrowInvalidCastException =
typeof(LanguagePrimitives).GetMethod(nameof(LanguagePrimitives.ThrowInvalidCastException), staticFlags);
@ -786,6 +788,7 @@ namespace System.Management.Automation.Language
{
IAssignableValue av = left.GetAssignableValue();
ExpressionType et = ExpressionType.Extension;
switch (tokenKind)
{
case TokenKind.Equals: return av.SetValue(this, right);
@ -794,15 +797,49 @@ namespace System.Management.Automation.Language
case TokenKind.MultiplyEquals: et = ExpressionType.Multiply; break;
case TokenKind.DivideEquals: et = ExpressionType.Divide; break;
case TokenKind.RemainderEquals: et = ExpressionType.Modulo; break;
case TokenKind.QuestionQuestionEquals when ExperimentalFeature.IsEnabled("PSCoalescingOperators"): et = ExpressionType.Coalesce; break;
}
var exprs = new List<Expression>();
var temps = new List<ParameterExpression>();
var getExpr = av.GetValue(this, exprs, temps);
exprs.Add(av.SetValue(this, DynamicExpression.Dynamic(PSBinaryOperationBinder.Get(et), typeof(object), getExpr, right)));
if(et == ExpressionType.Coalesce)
{
exprs.Add(av.SetValue(this, Coalesce(getExpr, right)));
}
else
{
exprs.Add(av.SetValue(this, DynamicExpression.Dynamic(PSBinaryOperationBinder.Get(et), typeof(object), getExpr, right)));
}
return Expression.Block(temps, exprs);
}
private static Expression Coalesce(Expression left, Expression right)
{
Type leftType = left.Type;
if (leftType.IsValueType)
{
return left;
}
else if(leftType == typeof(DBNull) || leftType == typeof(NullString) || leftType == typeof(AutomationNull))
{
return right;
}
else
{
Expression lhs = left.Cast(typeof(object));
Expression rhs = right.Cast(typeof(object));
return Expression.Condition(
Expression.Call(CachedReflectionInfo.LanguagePrimitives_IsNullLike, lhs),
rhs,
lhs);
}
}
internal Expression GetLocal(int tupleIndex)
{
Expression result = LocalVariablesParameter;
@ -5231,6 +5268,8 @@ namespace System.Management.Automation.Language
CachedReflectionInfo.ParserOps_SplitOperator,
_executionContextParameter, Expression.Constant(binaryExpressionAst.ErrorPosition), lhs.Cast(typeof(object)), rhs.Cast(typeof(object)),
ExpressionCache.Constant(false));
case TokenKind.QuestionQuestion when ExperimentalFeature.IsEnabled("PSCoalescingOperators"):
return Coalesce(lhs, rhs);
}
throw new InvalidOperationException("Unknown token in binary operator.");

View file

@ -6555,8 +6555,12 @@ namespace System.Management.Automation.Language
// G bitwise-expression '-bxor' new-lines:opt comparison-expression
// G
// G comparison-expression:
// G nullcoalesce-expression
// G comparison-expression comparison-operator new-lines:opt nullcoalesce-expression
// G
// G nullcoalesce-expression:
// G additive-expression
// G comparison-expression comparison-operator new-lines:opt additive-expression
// G nullcoalesce-expression '??' new-lines:opt additive-expression
// G
// G additive-expression:
// G multiplicative-expression

View file

@ -416,6 +416,12 @@ namespace System.Management.Automation.Language
/// <summary>The ternary operator '?'.</summary>
QuestionMark = 100,
/// <summary>The null conditional assignment operator '??='.</summary>
QuestionQuestionEquals = 101,
/// <summary>The null coalesce operator '??'.</summary>
QuestionQuestion = 102,
#endregion Operators
#region Keywords
@ -592,46 +598,51 @@ namespace System.Management.Automation.Language
/// <summary>
/// The precedence of the logical operators '-and', '-or', and '-xor'.
/// </summary>
BinaryPrecedenceLogical = 1,
BinaryPrecedenceLogical = 0x1,
/// <summary>
/// The precedence of the bitwise operators '-band', '-bor', and '-bxor'
/// </summary>
BinaryPrecedenceBitwise = 2,
BinaryPrecedenceBitwise = 0x2,
/// <summary>
/// The precedence of comparison operators including: '-eq', '-ne', '-ge', '-gt', '-lt', '-le', '-like', '-notlike',
/// '-match', '-notmatch', '-replace', '-contains', '-notcontains', '-in', '-notin', '-split', '-join', '-is', '-isnot', '-as',
/// and all of the case sensitive variants of these operators, if they exists.
/// </summary>
BinaryPrecedenceComparison = 3,
BinaryPrecedenceComparison = 0x5,
/// <summary>
/// The precedence of null coalesce operator '??'.
/// </summary>
BinaryPrecedenceCoalesce = 0x7,
/// <summary>
/// The precedence of the binary operators '+' and '-'.
/// </summary>
BinaryPrecedenceAdd = 4,
BinaryPrecedenceAdd = 0x9,
/// <summary>
/// The precedence of the operators '*', '/', and '%'.
/// </summary>
BinaryPrecedenceMultiply = 5,
BinaryPrecedenceMultiply = 0xa,
/// <summary>
/// The precedence of the '-f' operator.
/// </summary>
BinaryPrecedenceFormat = 6,
BinaryPrecedenceFormat = 0xc,
/// <summary>
/// The precedence of the '..' operator.
/// </summary>
BinaryPrecedenceRange = 7,
BinaryPrecedenceRange = 0xd,
#endregion Precedence Values
/// <summary>
/// A bitmask to get the precedence of binary operators.
/// </summary>
BinaryPrecedenceMask = 0x00000007,
BinaryPrecedenceMask = 0x0000000f,
/// <summary>
/// The token is a keyword.
@ -669,7 +680,7 @@ namespace System.Management.Automation.Language
SpecialOperator = 0x00001000,
/// <summary>
/// The token is one of the assignment operators: '=', '+=', '-=', '*=', '/=', or '%='
/// The token is one of the assignment operators: '=', '+=', '-=', '*=', '/=', '%=' or '??='
/// </summary>
AssignmentOperator = 0x00002000,
@ -854,8 +865,8 @@ namespace System.Management.Automation.Language
/* Shr */ TokenFlags.BinaryOperator | TokenFlags.BinaryPrecedenceComparison | TokenFlags.CanConstantFold,
/* Colon */ TokenFlags.SpecialOperator | TokenFlags.DisallowedInRestrictedMode,
/* QuestionMark */ TokenFlags.TernaryOperator | TokenFlags.DisallowedInRestrictedMode,
/* Reserved slot 3 */ TokenFlags.None,
/* Reserved slot 4 */ TokenFlags.None,
/* QuestionQuestionEquals */ TokenFlags.AssignmentOperator,
/* QuestionQuestion */ TokenFlags.BinaryOperator | TokenFlags.BinaryPrecedenceCoalesce,
/* Reserved slot 5 */ TokenFlags.None,
/* Reserved slot 6 */ TokenFlags.None,
/* Reserved slot 7 */ TokenFlags.None,
@ -1052,8 +1063,8 @@ namespace System.Management.Automation.Language
/* Shr */ "-shr",
/* Colon */ ":",
/* QuestionMark */ "?",
/* Reserved slot 3 */ string.Empty,
/* Reserved slot 4 */ string.Empty,
/* QuestionQuestionEquals */ "??=",
/* QuestionQuestion */ "??",
/* Reserved slot 5 */ string.Empty,
/* Reserved slot 6 */ string.Empty,
/* Reserved slot 7 */ string.Empty,

View file

@ -4994,6 +4994,25 @@ namespace System.Management.Automation.Language
return this.NewToken(TokenKind.Colon);
case '?' when InExpressionMode():
if (ExperimentalFeature.IsEnabled("PSCoalescingOperators"))
{
c1 = PeekChar();
if (c1 == '?')
{
SkipChar();
c1 = PeekChar();
if (c1 == '=')
{
SkipChar();
return this.NewToken(TokenKind.QuestionQuestionEquals);
}
return this.NewToken(TokenKind.QuestionQuestion);
}
}
return this.NewToken(TokenKind.QuestionMark);
case '\0':

View file

@ -0,0 +1,266 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
Describe 'NullConditionalOperations' -Tags 'CI' {
BeforeAll {
$skipTest = -not $EnabledExperimentalFeatures.Contains('PSCoalescingOperators')
if ($skipTest) {
Write-Verbose "Test Suite Skipped. The test suite requires the experimental feature 'PSCoalescingOperators' to be enabled." -Verbose
$originalDefaultParameterValues = $PSDefaultParameterValues.Clone()
$PSDefaultParameterValues["it:skip"] = $true
} else {
$someGuid = New-Guid
$typesTests = @(
@{ name = 'string'; valueToSet = 'hello' }
@{ name = 'dotnetType'; valueToSet = $someGuid }
@{ name = 'byte'; valueToSet = [byte]0x94 }
@{ name = 'intArray'; valueToSet = 1..2 }
@{ name = 'stringArray'; valueToSet = 'a'..'c' }
@{ name = 'emptyArray'; valueToSet = @(1, 2, 3) }
)
}
}
AfterAll {
if ($skipTest) {
$global:PSDefaultParameterValues = $originalDefaultParameterValues
}
}
Context "Null conditional assignment operator ??=" {
It 'Variable doesnot exist' {
Remove-Variable variableDoesNotExist -ErrorAction SilentlyContinue -Force
$variableDoesNotExist ??= 1
$variableDoesNotExist | Should -Be 1
$variableDoesNotExist ??= 2
$variableDoesNotExist | Should -Be 1
}
It 'Variable exists and is null' {
$variableDoesNotExist = $null
$variableDoesNotExist ??= 2
$variableDoesNotExist | Should -Be 2
}
It 'Validate types - <name> can be set' -TestCases $typesTests {
param ($name, $valueToSet)
$x = $null
$x ??= $valueToSet
$x | Should -Be $valueToSet
}
It 'Validate hashtable can be set' {
$x = $null
$x ??= @{ 1 = '1' }
$x.Keys | Should -Be @(1)
}
It 'Validate lhs is returned' {
$x = 100
$x ??= 200
$x | Should -Be 100
}
It 'Rhs is a cmdlet' {
$x = $null
$x ??= (Get-Alias -Name 'where')
$x.Definition | Should -BeExactly 'Where-Object'
}
It 'Lhs is DBNull' {
$x = [System.DBNull]::Value
$x ??= 200
$x | Should -Be 200
}
It 'Lhs is AutomationNull' {
$x = [System.Management.Automation.Internal.AutomationNull]::Value
$x ??= 200
$x | Should -Be 200
}
It 'Lhs is NullString' {
$x = [NullString]::Value
$x ??= 200
$x | Should -Be 200
}
It 'Lhs is empty string' {
$x = ''
$x ??= 20
$x | Should -BeExactly ''
}
It 'Error case' {
$e = $null
$null = [System.Management.Automation.Language.Parser]::ParseInput('1 ??= 100', [ref] $null, [ref] $e)
$e[0].ErrorId | Should -BeExactly 'InvalidLeftHandSide'
}
It 'Variable is non-null' {
$num = 10
$num ??= 20
$num | Should -Be 10
}
It 'Lhs is $?' {
{ $???=$false}
$? | Should -BeTrue
}
}
Context 'Null coalesce operator ??' {
BeforeEach {
$x = $null
}
It 'Variable does not exist' {
Remove-Variable variableDoesNotExist -ErrorAction SilentlyContinue -Force
$variableDoesNotExist ?? 100 | Should -Be 100
}
It 'Variable exists but is null' {
$x ?? 100 | Should -Be 100
}
It 'Lhs is not null' {
$x = 100
$x ?? 200 | Should -Be 100
}
It 'Lhs is a non-null constant' {
1 ?? 2 | Should -Be 1
}
It 'Lhs is `$null' {
$null ?? 'string value' | Should -BeExactly 'string value'
}
It 'Check precedence of ?? expression resolution' {
$x ?? $null ?? 100 | Should -Be 100
$null ?? $null ?? 100 | Should -Be 100
$null ?? $null ?? $null | Should -Be $null
$x ?? 200 ?? $null | Should -Be 200
$x ?? 200 ?? 300 | Should -Be 200
100 ?? $x ?? 200 | Should -Be 100
$null ?? 100 ?? $null ?? 200 | Should -Be 100
}
It 'Rhs is a cmdlet' {
$result = $x ?? (Get-Alias -Name 'where')
$result.Definition | Should -BeExactly 'Where-Object'
}
It 'Lhs is DBNull' {
$x = [System.DBNull]::Value
$x ?? 200 | Should -Be 200
}
It 'Lhs is AutomationNull' {
$x = [System.Management.Automation.Internal.AutomationNull]::Value
$x ?? 200 | Should -Be 200
}
It 'Lhs is NullString' {
$x = [NullString]::Value
$x ?? 200 | Should -Be 200
}
It 'Rhs is a get variable expression' {
$x = [System.DBNull]::Value
$y = 2
$x ?? $y | Should -Be 2
}
It 'Lhs is a constant' {
[System.DBNull]::Value ?? 2 | Should -Be 2
}
It 'Both are null constants' {
[System.DBNull]::Value ?? [NullString]::Value | Should -Be ([NullString]::Value)
}
It 'Lhs is $?' {
{$???$false} | Should -BeTrue
}
}
Context 'Null Coalesce ?? operator precedence' {
It '?? precedence over -and' {
$true -and $null ?? $true | Should -BeTrue
}
It '?? precedence over -band' {
1 -band $null ?? 1 | Should -Be 1
}
It '?? precedence over -eq' {
'x' -eq $null ?? 'x' | Should -BeTrue
$null -eq $null ?? 'x' | Should -BeFalse
}
It '?? precedence over -as' {
'abc' -as [datetime] ?? 1 | Should -BeNullOrEmpty
}
It '?? precedence over -replace' {
'x' -replace 'x',$null ?? 1 | Should -Be ([string]::empty)
}
It '+ precedence over ??' {
2 + $null ?? 3 | Should -Be 2
}
It '* precedence over ??' {
2 * $null ?? 3 | Should -Be 0
}
It '-f precedence over ??' {
"{0}" -f $null ?? 'b' | Should -Be ([string]::empty)
}
It '.. precedence ove ??' {
1..$null ?? 2 | Should -BeIn 1,0
}
}
Context 'Combined usage of null conditional operators' {
BeforeAll {
function GetNull {
return $null
}
function GetHello {
return "Hello"
}
}
BeforeEach {
$x = $null
}
It '?? and ??= used together' {
$x ??= 100 ?? 200
$x | Should -Be 100
}
It '?? and ??= chaining' {
$x ??= $x ?? (GetNull) ?? (GetHello)
$x | Should -BeExactly 'Hello'
}
It 'First two are null' {
$z ??= $null ?? 100
$z | Should -Be 100
}
}
}

View file

@ -262,6 +262,52 @@ Describe 'assignment statement parsing' -Tags "CI" {
ShouldBeParseError '$a,$b += 1,2' InvalidLeftHandSide 0
}
Describe 'null coalescing assignment statement parsing' -Tag 'CI' {
BeforeAll {
$skipTest = -not $EnabledExperimentalFeatures.Contains('PSCoalescingOperators')
if ($skipTest) {
Write-Verbose "Test Suite Skipped. The test suite requires the experimental feature 'PSCoalescingOperators' to be enabled." -Verbose
$originalDefaultParameterValues = $PSDefaultParameterValues.Clone()
$PSDefaultParameterValues["it:skip"] = $true
}
}
AfterAll {
if ($skipTest) {
$global:PSDefaultParameterValues = $originalDefaultParameterValues
}
}
ShouldBeParseError '1 ??= 1' InvalidLeftHandSide 0
ShouldBeParseError '@() ??= 1' InvalidLeftHandSide 0
ShouldBeParseError '@{} ??= 1' InvalidLeftHandSide 0
ShouldBeParseError '1..2 ??= 1' InvalidLeftHandSide 0
ShouldBeParseError '[int] ??= 1' InvalidLeftHandSide 0
ShouldBeParseError '$cricket ?= $soccer' ExpectedValueExpression,InvalidLeftHandSide 10,0
}
Describe 'null coalescing statement parsing' -Tag "CI" {
BeforeAll {
$skipTest = -not $EnabledExperimentalFeatures.Contains('PSCoalescingOperators')
if ($skipTest) {
Write-Verbose "Test Suite Skipped. The test suite requires the experimental feature 'PSCoalescingOperators' to be enabled." -Verbose
$originalDefaultParameterValues = $PSDefaultParameterValues.Clone()
$PSDefaultParameterValues["it:skip"] = $true
}
}
AfterAll {
if ($skipTest) {
$global:PSDefaultParameterValues = $originalDefaultParameterValues
}
}
ShouldBeParseError '$x??=' ExpectedValueExpression 5
ShouldBeParseError '$x ??Get-Thing' ExpectedValueExpression,UnexpectedToken 5,5
ShouldBeParseError '$??=$false' ExpectedValueExpression,InvalidLeftHandSide 3,0
ShouldBeParseError '$hello ??? $what' ExpectedValueExpression,MissingColonInTernaryExpression 9,17
}
Describe 'splatting parsing' -Tags "CI" {
ShouldBeParseError '@a' SplattingNotPermitted 0
ShouldBeParseError 'foreach (@a in $b) {}' SplattingNotPermitted 9