Skip to content

Implement Null Coalescing and Null Coalescing assignment operators #10636

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

Merged
merged 24 commits into from
Oct 17, 2019
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ecb94dd
Implement null condtional assigment operator
adityapatwardhan Aug 15, 2019
15119cf
Implement ?? operator
adityapatwardhan Aug 22, 2019
0cb1592
Add QuestionDot token
adityapatwardhan Aug 28, 2019
cfd73f9
Fix issues after merge and test fix
adityapatwardhan Sep 25, 2019
137a37b
Change Null coalescing assigned operator to ??=
adityapatwardhan Sep 25, 2019
94ad01f
Make feature experimental
adityapatwardhan Sep 26, 2019
dee8092
Add logic for skipping tests if experimental feature is disabled
adityapatwardhan Sep 27, 2019
9826e69
Fix parsing tests
adityapatwardhan Sep 27, 2019
9e599c4
Address code review feedback
adityapatwardhan Oct 1, 2019
8fd9572
Remove parsing test as it interfers with ternary operator
adityapatwardhan Oct 2, 2019
02dc105
Add few more tests
adityapatwardhan Oct 2, 2019
4e8019e
Address Rob's feedback
adityapatwardhan Oct 4, 2019
a9af2ab
Refactor according to feedback
adityapatwardhan Oct 9, 2019
0e93e14
Add coalesce assignment
adityapatwardhan Oct 9, 2019
771a86d
Add precedence flag for null coalesce operator and add tests
adityapatwardhan Oct 10, 2019
1a4887e
Merge branch 'master' into NullAssignment
adityapatwardhan Oct 10, 2019
73b8982
Revert formatting changes in token.cs
adityapatwardhan Oct 10, 2019
36f4b85
Fix parsing test
adityapatwardhan Oct 10, 2019
d530fcd
More test fixes
adityapatwardhan Oct 10, 2019
3f0f3d1
Check for experimental feature in parsing.tests.ps1
adityapatwardhan Oct 11, 2019
8f39bbb
Updated to move Coalesce code out of Binder
adityapatwardhan Oct 11, 2019
dea5793
Update TokenFlag name
adityapatwardhan Oct 14, 2019
4cf6ba4
Address feedback
adityapatwardhan Oct 15, 2019
268a609
Address Ilya's feedback
adityapatwardhan Oct 15, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,10 @@ static ExperimentalFeature()
description: "New parameter set for ForEach-Object to run script blocks in parallel"),
new ExperimentalFeature(
name: "PSTernaryOperator",
description: "Support the ternary operator in PowerShell language")
description: "Support the ternary operator in PowerShell language"),
new ExperimentalFeature(
name: "PSNullCoalescingOperators",
description: "Support the null coalescing operator and null coalescing assignment operator in PowerShell language")
};
EngineExperimentalFeatures = new ReadOnlyCollection<ExperimentalFeature>(engineFeatures);

Expand Down
37 changes: 37 additions & 0 deletions src/System.Management.Automation/engine/parser/Compiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ internal static class CachedReflectionInfo

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);

Expand Down Expand Up @@ -794,6 +796,7 @@ internal Expression ReduceAssignment(ISupportsAssignment left, TokenKind tokenKi
case TokenKind.MultiplyEquals: et = ExpressionType.Multiply; break;
case TokenKind.DivideEquals: et = ExpressionType.Divide; break;
case TokenKind.RemainderEquals: et = ExpressionType.Modulo; break;
case TokenKind.QuestionQuestionEquals: return GetCoalesceExpression(right, av, tokenKind);
}

var exprs = new List<Expression>();
Expand All @@ -803,6 +806,28 @@ internal Expression ReduceAssignment(ISupportsAssignment left, TokenKind tokenKi
return Expression.Block(temps, exprs);
}

private Expression GetCoalesceExpression(Expression rhs, IAssignableValue leftAssignableValue, TokenKind tokenKind)
{
if (ExperimentalFeature.IsEnabled("PSNullCoalescingOperators"))
{
var exprs = new List<Expression>();
var temps = new List<ParameterExpression>();
var leftExpr = leftAssignableValue.GetValue(this, exprs, temps);

if (tokenKind == TokenKind.QuestionQuestionEquals)
{
exprs.Add(
Expression.Condition(
Expression.Call(CachedReflectionInfo.LanguagePrimitives_IsNullLike, leftExpr),
leftAssignableValue.SetValue(this, rhs),
leftExpr.Convert(typeof(object))));
return Expression.Block(temps, exprs);
}
}

return null;
}

internal Expression GetLocal(int tupleIndex)
{
Expression result = LocalVariablesParameter;
Expand Down Expand Up @@ -5231,6 +5256,18 @@ public object VisitBinaryExpression(BinaryExpressionAst binaryExpressionAst)
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("PSNullCoalescingOperators") :
if (lhs is ConstantExpression lhsConstExpr && lhsConstExpr.Value != null)
{
return lhs;
}
else
{
return Expression.Condition(
Expression.Call(CachedReflectionInfo.LanguagePrimitives_IsNullLike, lhs),
rhs.Convert(typeof(object)),
lhs.Convert(typeof(object)));
}
}

throw new InvalidOperationException("Unknown token in binary operator.");
Expand Down
16 changes: 11 additions & 5 deletions src/System.Management.Automation/engine/parser/token.cs
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,12 @@ public enum TokenKind
/// <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
Expand Down Expand Up @@ -669,7 +675,7 @@ public enum TokenFlags
SpecialOperator = 0x00001000,

/// <summary>
/// The token is one of the assignment operators: '=', '+=', '-=', '*=', '/=', or '%='
/// The token is one of the assignment operators: '=', '+=', '-=', '*=', '/=', '%=' or '??='
/// </summary>
AssignmentOperator = 0x00002000,

Expand Down Expand Up @@ -854,8 +860,8 @@ public static class TokenTraits
/* 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,
/* Reserved slot 5 */ TokenFlags.None,
/* Reserved slot 6 */ TokenFlags.None,
/* Reserved slot 7 */ TokenFlags.None,
Expand Down Expand Up @@ -1051,8 +1057,8 @@ public static class TokenTraits
/* Shl */ "-shl",
/* Shr */ "-shr",
/* Colon */ ":",
/* Reserved slot 2 */ string.Empty,
/* Reserved slot 3 */ string.Empty,
/* QuestionQuestionEquals */ "??=",
/* QuestionQuestion */ "??",
/* Reserved slot 4 */ string.Empty,
/* Reserved slot 5 */ string.Empty,
/* Reserved slot 6 */ string.Empty,
Expand Down
19 changes: 19 additions & 0 deletions src/System.Management.Automation/engine/parser/tokenizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4994,6 +4994,25 @@ internal Token NextToken()
return this.NewToken(TokenKind.Colon);

case '?' when InExpressionMode():
c1 = PeekChar();

if (ExperimentalFeature.IsEnabled("PSNullCoalescingOperators"))
{
if (c1 == '?')
{
SkipChar();
c1 = PeekChar();

if (c1 == '=')
{
SkipChar();
return CheckOperatorInCommandMode(c, c1, TokenKind.QuestionQuestionEquals);
}

return CheckOperatorInCommandMode(c, c1, TokenKind.QuestionQuestion);
}
}

return this.NewToken(TokenKind.QuestionMark);

case '\0':
Expand Down
198 changes: 198 additions & 0 deletions test/powershell/Language/Operators/NullConditional.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

Describe 'NullConditionalOperations' -Tags 'CI' {
BeforeAll {

$skipTest = -not $EnabledExperimentalFeatures.Contains('PSNullCoalescingOperators')

if ($skipTest) {
Write-Verbose "Test Suite Skipped. The test suite requires the experimental feature 'PSNullCoalescingOperators' 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 ??= (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 'Error case' {
$e = $null
$null = [System.Management.Automation.Language.Parser]::ParseInput('1 ??= 100', [ref] $null, [ref] $e)
$e[0].ErrorId | Should -BeExactly 'InvalidLeftHandSide'
}
}

Context 'Null coalesce operator ??' {
BeforeEach {
$x = $null
}

It 'Variable does not exist' {
$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)
}
}

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'
}
}
}
6 changes: 6 additions & 0 deletions test/powershell/Language/Parser/Parsing.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,12 @@ Describe 'function statement parsing' -Tags "CI" {

Describe 'assignment statement parsing' -Tags "CI" {
ShouldBeParseError '$a,$b += 1,2' InvalidLeftHandSide 0
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 '(Get-Variable x) ??= 1' InvalidLeftHandSide 0
}

Describe 'splatting parsing' -Tags "CI" {
Expand Down