Skip to content
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

Feature: An alternative approach to handling "null conditional" ops #251

Merged
merged 23 commits into from
Oct 20, 2021
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f4351c6
First pass at an alternative approach to handling "null conditional" …
Nigel-Ecma Apr 5, 2021
f881f34
Update expressions.md
Nigel-Ecma Apr 28, 2021
91fe31b
Update lexical-structure.md
Nigel-Ecma Apr 28, 2021
a98d5d7
Update statements.md
Nigel-Ecma Apr 28, 2021
4ee7b60
Extend grammar of null_conditional_member_access and null_conditional…
Nigel-Ecma May 3, 2021
64659e0
Missed one alternative of unconditional_access_part
Nigel-Ecma May 3, 2021
247e2be
Merge branch 'dotnet:draft-v6' into alt-null-conditional-ops
Nigel-Ecma May 31, 2021
d264cee
Updated to now include:
Nigel-Ecma May 31, 2021
c238d13
fix adjacent key typo
Nigel-Ecma May 31, 2021
d147b8b
Missed out lexical-structure.md
Nigel-Ecma May 31, 2021
08607f9
Update standard/expressions.md
Nigel-Ecma Jun 25, 2021
854f9d6
Update standard/expressions.md
Nigel-Ecma Jun 25, 2021
5ed92c2
- A number of small changes resolving issues raised in the comments.
Nigel-Ecma Jun 25, 2021
e4affe1
Fix some bad links
Nigel-Ecma Jun 25, 2021
181462d
And yet more link fixes...
Nigel-Ecma Jun 25, 2021
4d995f7
And I continue to whittle 'em down...
Nigel-Ecma Jun 25, 2021
1918dae
Addressed some issues raised and made a few corrections/improvements …
Nigel-Ecma Sep 14, 2021
78f7fba
Merge in changes to draft-v6 since all-null-conditional-ops branched off
Nigel-Ecma Sep 21, 2021
10c969b
Rename captured_access to dependent_access
Nigel-Ecma Sep 26, 2021
59226a8
Merge branch 'draft-v6' into alt-null-conditional-ops to bring in lat…
Nigel-Ecma Sep 27, 2021
04a3841
Merge in changes from draft-v6 and keep grammar.md up-to-date
Nigel-Ecma Sep 27, 2021
e1133a3
Apply suggestions from code review
Nigel-Ecma Oct 17, 2021
4457d3e
Fix editing snafu unearthed by Bill & Jon. Mea culpa.
Nigel-Ecma Oct 19, 2021
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
12 changes: 10 additions & 2 deletions standard/classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -1622,12 +1622,20 @@ member_name

method_body
: block
| '=>' null_conditional_invocation_expression ';'
| '=>' expression ';'
| ';'
;
```

*unsafe_modifier* ([§23.2](unsafe-code.md#232-unsafe-contexts)) is only available in unsafe code ([§23](unsafe-code.md#23-unsafe-code)).
Grammar notes:

- *unsafe_modifier* ([§23.2](unsafe-code.md#232-unsafe-contexts)) is only available in unsafe code ([§23](unsafe-code.md#23-unsafe-code)).

jskeet marked this conversation as resolved.
Show resolved Hide resolved
- when recognising a *method_body* if both the *null_conditional_invocation_expression* and *expression* alternatives are applicable then the former shall be chosen.

> *Note*: The overlapping of, and priority between, alternatives here is solely for descriptive convenience; the grammar rules could be elaborated to remove the overlap. ANTLR, and other grammar systems, adopt the same convenience and so *method_body* has the specified semantics automatically.


A *method_declaration* may include a set of *attributes* ([§22](attributes.md#22-attributes)) and a valid combination of the four access modifiers ([§15.3.6](classes.md#1536-access-modifiers)), the `new` ([§15.3.5](classes.md#1535-the-new-modifier)), `static` ([§15.6.3](classes.md#1563-static-and-instance-methods)), `virtual` ([§15.6.4](classes.md#1564-virtual-methods)), `override` ([§15.6.5](classes.md#1565-override-methods)), `sealed` ([§15.6.6](classes.md#1566-sealed-methods)), `abstract` ([§15.6.7](classes.md#1567-abstract-methods)), `extern` ([§15.6.8](classes.md#1568-external-methods)) and `async` ([§15.15](classes.md#1515-async-functions)) modifiers.

Expand All @@ -1650,7 +1658,7 @@ The optional *formal_parameter_list* specifies the parameters of the method ([§

The *return_type* and each of the types referenced in the *formal_parameter_list* of a method shall be at least as accessible as the method itself ([§8.5.5](basic-concepts.md#855-accessibility-constraints)).

The *method_body* is either a semicolon, a ***block body*** or an ***expression body***. A block body consists of a *block*, which specifies the statements to execute when the method is invoked. An expression body consists of `=>` followed by an *expression* and a semicolon, and denotes a single expression to perform when the method is invoked.
The *method_body* is either a semicolon, a ***block body*** or an ***expression body***. A block body consists of a *block*, which specifies the statements to execute when the method is invoked. An expression body consists of `=>`, followed by a *null_conditional_invocation_expression* or *expression*, followed by a semicolon, and denotes a single expression to perform when the method is invoked.

For abstract and extern methods, the *method_body* consists simply of a semicolon. For partial methods the *method_body* may consist of either a semicolon, a block body or an expression body. For all other methods, the *method_body* is either a block body or an expression body.

Expand Down
120 changes: 115 additions & 5 deletions standard/expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ The precedence of an operator is established by the definition of its associated
> *Note*: The following table summarizes all operators in order of precedence from highest to lowest:
> **Subclause** | **Category** | **Operators**
> --------------- | ------------------------------- | -------------------------------------------------------
> [§12.7](expressions.md#127-primary-expressions) | Primary | `x.y` `f(x)` `a[x]` `x++` `x--` `new` `typeof` `default` `checked` `unchecked` `delegate`
> [§12.7](expressions.md#127-primary-expressions) | Primary | `x.y` `x?.y` `f(x)` `a[x]` `a?[x]` `x++` `x--` `new` `typeof` `default` `checked` `unchecked` `delegate`
> [§12.8](expressions.md#128-unary-operators) | Unary | `+` `-` `!` `~` `++x` `--x` `(T)x` `await x`
> [§12.9](expressions.md#129-arithmetic-operators) | Multiplicative | `*` `/` `%`
> [§12.9](expressions.md#129-arithmetic-operators) | Additive | `+` `-`
Expand Down Expand Up @@ -1109,8 +1109,10 @@ primary_no_array_creation_expression
| simple_name
| parenthesized_expression
| member_access
| null_conditional_member_access
| invocation_expression
| element_access
| null_conditional_element_access
| this_access
| base_access
| post_increment_expression
Expand Down Expand Up @@ -1283,6 +1285,50 @@ In a member access of the form `E.I`, if `E` is a single identifier, and if the
> ```
> Within the `A` class, those occurrences of the Color identifier that reference the Color type are delimited by `**`, and those that reference the Color field are not. *end example*

### §null-conditional-member-access Null Conditional Member Access
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just been trying to figure out how this works in a couple of cases, and while I think it's the right approach, we may need a bit more explanation.

For example consider:

string x = null;
int? y = x?.GetHashCode();

My understanding is that that is a null_conditional_member_access with a dependent_access of () - so we get the null value. It's not a null_conditional_invocation_expression, because that's only used in places where the result (if any) is discarded. Is that the case? If so, it's a bit confusing because x.GetHashCode() is an invocation expresson, isn't it?

I'm not suggesting that we change any of the existing text - just consider adding an example/note.


A *null_conditional_member_access* is a conditional version of *member_access* ([§12.7.5](expressions.md#1275-member-access)) and it is a binding time error if the result type is `void`. For a null conditional expression where the result type may be `void` see (§null-conditional-invocation-expression).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: double space in "and it"


A *null_conditional_member_access* consists of a *primary_expression* followed by the two tokens "`?`" and "`.`", followed by an *Identifier* with an optional *type_argument_list*, followed by zero or more *dependent_access*es.

```ANTLR
null_conditional_member_access
: primary_expression '?' '.' Identifier type_argument_list? dependent_access*
;

dependent_access
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to check, primary_expression will cover nested null-conditional member accesses, yes? So if we have:

x?.M1()?.M2().M3

that ends up with a "top-level" null_conditional_member_access where the primary_expression is x?.M1()?.M2(), which is itself a null_conditional_member_access with a primary_expression of x?.M1(), which is itself a null_conditional_member_access with a primary expression of x... is that right?

: '.' Identifier type_argument_list? // member access
| '[' argument_list ']' // element access
| '(' argument_list? ')' // invocation
;

null_conditional_projection_initializer
: primary_expression '?' '.' Identifier type_argument_list?
;
```

A *null_conditional_member_access* expression `E` is of the form `P?.A`. Let `T` be the type of the expression `P.A`. The meaning of `E` is determined as follows:

- If `T` is a type parameter that is not known to be a reference type or a non-nullable value type, a compile-time error occurs.
- If `T` is a non-nullable value type, then the type of `E` is `T?`, and the meaning of `E` is the same as the meaning of:
> ```csharp
> ((object)P == null) ? (T?)null : P.A
> ```
Except that `P` is evaluated only once.
- Otherwise the type of `E` is `T`, and the meaning of `E` is the same as the meaning of:
Nigel-Ecma marked this conversation as resolved.
Show resolved Hide resolved
> ```csharp
> ((object)P == null) ? null : P.A
> ```
Except that `P` is evaluated only once.

*Note*: In an expression of the form:
> ```csharp
> P?.A₀?.A₁
> ```
then if `P` evaluates to `null` neither `A₀` or `A₁` are evaluated. The same is true if an expression is a sequence of *null_conditional_member_access* or *null_conditional_element_access* §null-conditional-element-access operations. *end note*

A *null_conditional_projection_initializer* is a restriction of *null_conditional_member_access* and has the same semantics. It only occurs as a projection initializer in an anonymous object creation expression ([§12.7.11.7](expressions.md#127117-anonymous-object-creation-expressions)).

### 12.7.6 Invocation expressions

#### 12.7.6.1 General
Expand Down Expand Up @@ -1467,6 +1513,31 @@ The run-time processing of a delegate invocation of the form `D(A)`, where `D`

See [§20.6](delegates.md#206-delegate-invocation) for details of multiple invocation lists without parameters.

### §null-conditional-invocation-expression Null Conditional Invocation Expression
Nigel-Ecma marked this conversation as resolved.
Show resolved Hide resolved

A *null_conditional_invocation_expression* is syntactically either a *null_conditional_member_access* (§null-conditional-member-access) or *null_conditional_element_access* (§null-conditional-element-access) where the final *dependent_access* is an invocation expression ([§12.7.6](expressions.md#1276-invocation-expressions)).

A *null_conditional_invocation_expression* occurs within the context of a *statement_expression* ([§13.7](statements.md#137-expression-statements)), *anonymous_function_body* ([§12.16.1](expressions.md#12161-general)), or *method_body* ([§15.6.1](classes.md#1561-general)).

Unlike the syntactically equivalent *null_conditional_member_access* or *null_conditional_element_access*, a *null_conditional_invocation_expression* may be classified as nothing.

```ANTLR
null_conditional_invocation_expression
: null_conditional_member_access '(' argument_list? ')'
| null_conditional_element_access '(' argument_list? ')'
;
```

A *null_conditional_invocation_expression* expression `E` is of the form `P?A`; where `A` is the remainder of the syntactically equivalent *null_conditional_member_access* or *null_conditional_element_access*, `A` will therefore start with `.` or `[`. Let `PA` signify the concatention of `P` and `A`. When `E` occurs as a *statement_expression* the meaning of `E` is the same as the meaning of the *statement*:
> ```csharp
> if ((object)P != null) PA
> ```
When `E` occurs as a *anonymous_function_body* or *method_body* the meaning of `E` is the same as the meaning of the *block*:
> ```csharp
> { if ((object)P != null) PA; }
> ```
Except that in both cases `P` is evaluated only once.

### 12.7.7 Element access

#### 12.7.7.1 General
Expand Down Expand Up @@ -1521,6 +1592,38 @@ The binding-time processing of an indexer access of the form `P[A]`, where `P` i

Depending on the context in which it is used, an indexer access causes invocation of either the *get_accessor* or the *set_accessor* of the indexer. If the indexer access is the target of an assignment, the *set_accessor* is invoked to assign a new value ([§12.18.2](expressions.md#12182-simple-assignment)). In all other cases, the *get_accessor* is invoked to obtain the current value ([§12.2.2](expressions.md#1222-values-of-expressions)).

### §null-conditional-element-access Null Conditional Element Access

A *null_conditional_element_access* consists of a *primary_no_array_creation_expression* followed by the two tokens "`?`" and "`[`", followed by an *argument_list*, followed by a "`]`" token, followed by zero or more *dependent_access*es.

```ANTLR
null_conditional_element_access
: primary_no_array_creation_expression '?' '[' argument_list ']' dependent_access*
;
```

A *null_conditional_element_access* is a conditional version of *element_access* ([§12.7.7](expressions.md#1277-element-access)) and it is a binding time error if the result type is `void`. For a null conditional expression where the result type may be `void` see (§null-conditional-invocation-expression).
Nigel-Ecma marked this conversation as resolved.
Show resolved Hide resolved

A *null_conditional_element_access* expression `E` is of the form `P?[A]B`; where `B` are the *dependent_access*es, if any. Let `T` be the type of the expression `P[A]B`. The meaning of `E` is determined as follows:

- If `T` is a type parameter that is not known to be a reference type or a non-nullable value type, a compile-time error occurs.
- If `T` is a non-nullable value type, then the type of `E` is `T?`, and the meaning of `E` is the same as the meaning of:
> ```csharp
> ((object)P == null) ? (T?)null : P[A]B
> ```
Except that `P` is evaluated only once.
- Otherwise the type of `E` is `T`, and the meaning of `E` is the same as the meaning of:
> ```csharp
> ((object)P == null) ? null : P[A]B
> ```
Except that `P` is evaluated only once.

*Note*: In an expression of the form:
> ```csharp
> P?[A₀]?[A₁]
> ```
if `P` evaluates to `null` neither `A₀` or `A₁` are evaluated. The same is true if an expression is a sequence of *null_conditional_element_access* or *null_conditional_member_access* §null-conditional-member-access operations. *end note*

### 12.7.8 This access

A *this_access* consists of the keyword `this`.
Expand Down Expand Up @@ -2024,9 +2127,11 @@ member_declarator_list
member_declarator
: simple_name
| member_access
| null_conditional_projection_initializer
| base_access
| identifier '=' expression
;

```

An anonymous object initializer declares an anonymous type and returns an instance of that type. An anonymous type is a nameless class type that inherits directly from `object`. The members of an anonymous type are a sequence of read-only properties inferred from the anonymous object initializer used to create an instance of the type. Specifically, an anonymous object initializer of the form
Expand Down Expand Up @@ -2073,13 +2178,13 @@ Within the same program, two anonymous object initializers that specify a sequen

The `Equals` and `GetHashcode` methods on anonymous types override the methods inherited from `object`, and are defined in terms of the `Equals` and `GetHashcode` of the properties, so that two instances of the same anonymous type are equal if and only if all their properties are equal.

A member declarator can be abbreviated to a simple name ([§12.7.3](expressions.md#1273-simple-names)), a member access ([§12.7.5](expressions.md#1275-member-access)) or a base access ([§12.7.9](expressions.md#1279-base-access)). This is called a ***projection initializer*** and is shorthand for a declaration of and assignment to a property with the same name. Specifically, member declarators of the forms
A member declarator can be abbreviated to a simple name ([§12.7.3](expressions.md#1273-simple-names)), a member access ([§12.7.5](expressions.md#1275-member-access)), a null conditional projection initializer §null-conditional-member-access or a base access ([§12.7.9](expressions.md#1279-base-access)). This is called a ***projection initializer*** and is shorthand for a declaration of and assignment to a property with the same name. Specifically, member declarators of the forms

`«identifier»` and `«expr» . «identifier»`
`«identifier»`, `«expr» . «identifier»` and `«expr» ? . «identifier»`

are precisely equivalent to the following, respectively:

`«identifer» = «identifier»` and `«identifier» = «expr» . «identifier»`
`«identifer» = «identifier»`, `«identifier» = «expr» . «identifier»` and `«identifier» = «expr» ? . «identifier»`

Thus, in a projection initializer the identifier selects both the value and the field or property to which the value is assigned. Intuitively, a projection initializer projects not just a value, but also the name of the value.

Expand Down Expand Up @@ -4089,11 +4194,16 @@ implicit_anonymous_function_parameter
;

anonymous_function_body
: expression
: null_conditional_invocation_expression
| expression
| block
;
```

When recognising an *anonymous_function_body* if both the *null_conditional_invocation_expression* and *expression* alternatives are applicable then the former shall be chosen.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about this one. To go back to my previous example, consider:

Func<string, int?> func = x => x?.GetHashCode();

Does that satisfy the requirements of this paragraph? We'd still want to use an expression with a null_conditional_member_access rather than a null_conditional_invocation_expression in order to avoid "losing" the return value, wouldn't we? I may have missed something here.


> *Note*: The overlapping of, and priority between, alternatives here is solely for descriptive convenience; the grammar rules could be elaborated to remove the overlap. ANTLR, and other grammar systems, adopt the same convenience and so *anonymous_function_body* has the specified semantics automatically.

The `=>` operator has the same precedence as assignment (`=`) and is right-associative.

An anonymous function with the `async` modifier is an async function and follows the rules described in [§15.15](classes.md#1515-async-functions).
Expand Down
33 changes: 31 additions & 2 deletions standard/grammar.md
Original file line number Diff line number Diff line change
Expand Up @@ -675,8 +675,10 @@ primary_no_array_creation_expression
| simple_name
| parenthesized_expression
| member_access
| null_conditional_member_access
| invocation_expression
| element_access
| null_conditional_element_access
| this_access
| base_access
| post_increment_expression
Expand Down Expand Up @@ -717,16 +719,39 @@ predefined_type
| 'object' | 'sbyte' | 'short' | 'string' | 'uint' | 'ulong' | 'ushort'
;

null_conditional_member_access
: primary_expression '?' '.' Identifier type_argument_list? dependent_access*
;

dependent_access
: '.' Identifier type_argument_list? // member access
| '[' argument_list ']' // element access
| '(' argument_list? ')' // invocation
;

null_conditional_projection_initializer
: primary_expression '?' '.' Identifier type_argument_list?
;

// Source: §12.7.6.1 General
invocation_expression
: primary_expression '(' argument_list? ')'
;

null_conditional_invocation_expression
: null_conditional_member_access '(' argument_list? ')'
| null_conditional_element_access '(' argument_list? ')'
;

// Source: §12.7.7.1 General
element_access
: primary_no_array_creation_expression '[' argument_list ']'
;

null_conditional_element_access
: primary_no_array_creation_expression '?' '[' argument_list ']' dependent_access*
;

// Source: §12.7.8 This access
this_access
: 'this'
Expand Down Expand Up @@ -826,6 +851,7 @@ member_declarator_list
member_declarator
: simple_name
| member_access
| null_conditional_projection_initializer
| base_access
| identifier '=' expression
;
Expand Down Expand Up @@ -1044,7 +1070,8 @@ implicit_anonymous_function_parameter
;

anonymous_function_body
: expression
: null_conditional_invocation_expression
| expression
| block
;

Expand Down Expand Up @@ -1252,7 +1279,8 @@ expression_statement
;

statement_expression
: invocation_expression
: null_conditional_invocation_expression
| invocation_expression
| object_creation_expression
| assignment
| post_increment_expression
Expand Down Expand Up @@ -1659,6 +1687,7 @@ member_name

method_body
: block
| '=>' null_conditional_invocation_expression ';'
| '=>' expression ';'
| ';'
;
Expand Down
3 changes: 2 additions & 1 deletion standard/statements.md
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,8 @@ expression_statement
;

statement_expression
: invocation_expression
: null_conditional_invocation_expression
| invocation_expression
| object_creation_expression
| assignment
| post_increment_expression
Expand Down