|
| 1 | +--- |
| 2 | +RFC: RFC0045 |
| 3 | +Author: Dongbo Wang |
| 4 | +Status: Experimental-Accepted |
| 5 | +SupercededBy: N/A |
| 6 | +Version: 0.1 |
| 7 | +Area: Language |
| 8 | +Comments Due: August 31st, 2019 |
| 9 | +Plan to implement: Yes, PS7 |
| 10 | +--- |
| 11 | + |
| 12 | +# Add Ternary Operator to PowerShell Language |
| 13 | + |
| 14 | +The ternary operator is one of the highly demanded enhancement to PowerShell language. |
| 15 | +It received 46 up-votes in the issue [Suggestion: implement ternary conditionals](https://github.com/PowerShell/PowerShell/issues/3239) as of the writing of this RFC. |
| 16 | +The [prototype draft pull request](https://github.com/PowerShell/PowerShell/pull/10161) for this feature has also received a lot of attention and discussions. |
| 17 | + |
| 18 | +The PowerShell Committee has approved to add the ternary operator to the language, |
| 19 | +and this RFC is mainly to capture the design points and implementation details. |
| 20 | + |
| 21 | +## Motivation |
| 22 | + |
| 23 | +> As a PowerShell user, I can use ternary operator as a replacement of if-else statement for simple conditional cases. |
| 24 | +
|
| 25 | +## Specification |
| 26 | + |
| 27 | +We will follow the C# ternary operator syntax: |
| 28 | + |
| 29 | +```none |
| 30 | +<condition> ? <if-true> : <if-false> |
| 31 | +``` |
| 32 | + |
| 33 | +Two basic design points: |
| 34 | + |
| 35 | +- Ternary operator produces an expression, just like the binary and unary operators |
| 36 | +- Ternary operator has lower precedence order than the binary operator |
| 37 | + |
| 38 | +The ternary operator behaves like the simplified if-else statement. |
| 39 | +The `condition-expression` will always be evaluated, |
| 40 | +and its result will be converted to boolean to determine which branch will be evaluated next: |
| 41 | + |
| 42 | +- `if-true-expression` will execute if the condition's result is evaluated as `true` |
| 43 | +- `if-false-expression` will execute if the condition's result is evaluated as `false` |
| 44 | + |
| 45 | +Some usage examples (see more examples [in the tests](https://github.com/PowerShell/PowerShell/pull/10367/files#diff-b63d92046948a571e210b3abb5a7e685)): |
| 46 | + |
| 47 | +```powershell |
| 48 | +... |
| 49 | +$psExec = $IsCoreCLR ? 'pwsh' : 'powershell' |
| 50 | +... |
| 51 | +
|
| 52 | +... |
| 53 | +$argument = $env:CI -eq 'true' ? '-Tag CI' : '-Tag Daily' |
| 54 | +... |
| 55 | +
|
| 56 | +... |
| 57 | +$message = (Test-Path $path) ? "Path exists" : "Path not found" |
| 58 | +... |
| 59 | +
|
| 60 | +... |
| 61 | +return $blackList -contains $target ? (Block-Target $target) : (Register-Target $target) |
| 62 | +... |
| 63 | +``` |
| 64 | + |
| 65 | +#### Parser Updates |
| 66 | + |
| 67 | +A new AST type `TernaryExpressionAst` will be introduced to represent a ternary operator expression. |
| 68 | +The public API surface of this type is as follows: |
| 69 | + |
| 70 | +```c# |
| 71 | +public class TernaryExpressionAst : ExpressionAst |
| 72 | +{ |
| 73 | + public TernaryExpressionAst(IScriptExtent extent, ExpressionAst condition, ExpressionAst ifTrue, ExpressionAst ifFalse); |
| 74 | + public ExpressionAst Condition { get; } |
| 75 | + public ExpressionAst IfTrue { get; } |
| 76 | + public ExpressionAst IfFalse { get; } |
| 77 | + |
| 78 | + public Ast Copy(); |
| 79 | +} |
| 80 | +``` |
| 81 | + |
| 82 | +The parsing grammar is as follows: |
| 83 | + |
| 84 | +```none |
| 85 | +<expression> '?' new-lines:opt <expression> new-lines:opt ':' new-lines:opt <expression> |
| 86 | +``` |
| 87 | + |
| 88 | +The challenge in the parser update is around how to deal with number constant expressions more naturally with the ternary operators. |
| 89 | +When digits are mixed with `?` or `:`, such as `?123`, `:456`, `123:` and `?123:456`, they are recognized as generic tokens today, |
| 90 | +because it's totally legit to have a function or script with those names. However, when it comes to the ternary expression, |
| 91 | +this means confusing errors would occur for scripts like `return ${succeed}?0:1` since `?0:1` would be treated as a generic token. |
| 92 | +Therefore, spaces around `?` and `:` would be required to use number constant expressions with the ternary operator. |
| 93 | +That would certainly be counter-intuitive and should be addressed in the parser update. |
| 94 | + |
| 95 | +The approach I took in the prototype is to make `?` and `:` conditionally force to start a new token when scanning for numbers. |
| 96 | +When it's known for sure that we are expecting an expression, |
| 97 | +allowing a generic token like `123?` is not useful and bound to result in parsing errors. |
| 98 | +In those cases, we will force to start a new token upon seeing characters `?` and `:` when scanning for a number, |
| 99 | +so that that number constant expressions can work with ternary operators more intuitively. |
| 100 | + |
| 101 | +Note that, the handling of `?` and `:` doesn't change how those two characters can be used in variable expressions, |
| 102 | +and also doesn't affect function and commands with those two characters in names. |
| 103 | + |
| 104 | +#### AST Visitors |
| 105 | + |
| 106 | +Given the fact that we are adding a new AST type, necessary changes are required to `ICustomAstVisitor2`, `AstVisitor2`, |
| 107 | +and all other visitor types that implemnent those two. |
| 108 | +Obvious examples are the `VariableAnalysis` and `Compiler` as you have to update them properly to correctly enable the new language element. |
| 109 | +The not-so-obvious ones include `ConstantValueVisitor`, `SafeValueVisitor`, `TypeInferenceVisitor` and more. |
| 110 | +Take the `TypeInferenceVisitor` as an instance, failing to update it may cause tab completion issues that are hard to catch. |
| 111 | + |
| 112 | +It's worth to discuss the `InternalVisit` implemention of the new AST type a bit. |
| 113 | +Many `Find*Visitor` types depend on this method to search through an AST tree, |
| 114 | +and almost all of them derive from `AstVisitor` instead of `AstVisitor2` because |
| 115 | +currently the AST types covered in `AstVisitor2` all have their own special scopes, |
| 116 | +such as the `TypeDefinitionAst` and `DynamicKeywordStatement`. |
| 117 | +In order to avoid making changes to all those existing `Find*Visitor` types, |
| 118 | +the `InternalVisit` method of `TernaryExpressionAst` is implemented as follows to accept an `AstVisitor` visitor for its AST members. |
| 119 | +In this way, those existing `Find*Visitor` types can work with this new AST type to search through its members. |
| 120 | + |
| 121 | +```c# |
| 122 | +// This pattern appears in several other ASTs. |
| 123 | +internal override AstVisitAction InternalVisit(AstVisitor visitor) |
| 124 | +{ |
| 125 | + var action = AstVisitAction.Continue; |
| 126 | + if (visitor is AstVisitor2 visitor2) |
| 127 | + { |
| 128 | + action = visitor2.VisitTernaryExpression(this); |
| 129 | + if (action == AstVisitAction.SkipChildren) |
| 130 | + return visitor.CheckForPostAction(this, AstVisitAction.Continue); |
| 131 | + } |
| 132 | + |
| 133 | + if (action == AstVisitAction.Continue) |
| 134 | + action = Condition.InternalVisit(visitor); |
| 135 | + |
| 136 | + if (action == AstVisitAction.Continue) |
| 137 | + action = IfTrue.InternalVisit(visitor); |
| 138 | + |
| 139 | + if (action == AstVisitAction.Continue) |
| 140 | + action = IfFalse.InternalVisit(visitor); |
| 141 | + |
| 142 | + return visitor.CheckForPostAction(this, action); |
| 143 | +} |
| 144 | +``` |
| 145 | + |
| 146 | +### Alternate Proposals and Considerations |
| 147 | + |
| 148 | +Different syntax proposals were brought up in the discussions: |
| 149 | + |
| 150 | +```none |
| 151 | +## Use '-then -else' instead of '? :' |
| 152 | +<condition-expression> -then <then-expression> -else <else-expression> |
| 153 | +``` |
| 154 | +```none |
| 155 | +## Use syntax similar to '-replace' and '-f' |
| 156 | +<condition-expression> -ternary <then-expression>, <else-expression> |
| 157 | +``` |
| 158 | + |
| 159 | +One of the concerns with using `? :` is the visual acuity impact of scanning code where `?` typically has only been used as the alias for `Where-Object`. |
| 160 | +However, we expect that as we implement other language features like null-coalescing, null-conditional assignment, and null-conditional access, |
| 161 | +we are likely to use `?` in some other forms. |
| 162 | +Also, users can choose to opt out of using this new language feature, |
| 163 | +and the expectation is that most usage of this new language element will be from C# developers. |
| 164 | + |
| 165 | +After a discussion at length, the PowerShell committee agrees to align with the C# syntax given the above considerations. |
0 commit comments