Compare commits

...

10 commits

Author SHA1 Message Date
Rex Jaeschke c1693320d6 Convert tuple equality proposal into spec text (#3206) 2021-04-19 12:47:30 -04:00
Rex Jaeschke 5ade4ff123 Move tuple literals from "Lexical structure" to "Expressions" (#3205) 2021-04-19 12:46:25 -04:00
Rex Jaeschke 4df9865acd Update digit-separators.md (#3203)
Remove trailing colons from production names in the grammar
2021-04-19 12:46:25 -04:00
Rex Jaeschke 1428bec95a Update binary-literals.md (#3202)
Remove trailing colon from some productions in the grammar
2021-04-19 12:46:25 -04:00
Rex Jaeschke 75153abf53 Update leading-separator.md (#3192) 2021-04-19 12:46:25 -04:00
Bill Wagner 87aa78f742 Tuples feedback (#3150)
* address first two changes.

* more edits on feedback

* address other comments.

* Address issues

All issues flagged in the article are either fixed, or logged as GitHub issues in the spec project.
2021-04-19 12:46:25 -04:00
Rex Jaeschke 0899831f86 Update tuples.md (#3180) 2021-04-19 12:46:25 -04:00
Rex Jaeschke 7e343a404b Update binary-literals.md (#3161) 2021-04-19 12:46:25 -04:00
Rex Jaeschke 7351ff1e34 Update digit-separators.md (#3162) 2021-04-19 12:46:24 -04:00
Bill Wagner ed10ce16e3 First set of changes for tuples from feature spec and LDM notes. (#3031)
* rough draft done

* proofread and edit.

* update text for standard style.
2021-04-19 12:46:24 -04:00
5 changed files with 920 additions and 88 deletions

View file

@ -1,36 +1,68 @@
# Binary literals
Theres a relatively common request to add binary literals to C# and VB. For bitmasks (e.g. flag enums) this seems genuinely useful, but it would also be great just for educational purposes.
This proposal specifies the changes required to the [C# 6.0 (draft) Language specification](../../spec/introduction.md) to support *binary integer literals*.
Binary literals would look like this:
## Changes to [Lexical structure](../../spec/lexical-structure.md)
```csharp
int nineteen = 0b10011;
```
### Literals
Syntactically and semantically they are identical to hexadecimal literals, except for using `b`/`B` instead of `x`/`X`, having only digits `0` and `1` and being interpreted in base 2 instead of 16.
#### Integer literals
Theres little cost to implementing these, and little conceptual overhead to users of the language.
> The grammar for [integer literals](../../spec/lexical-structure.md#Integer-literals) is extended to include `binary_integer_literal`:
## Syntax
The grammar would be as follows:
Integer literals are used to write values of types `int`, `uint`, `long`, and `ulong`. Integer literals have three possible forms: decimal, hexadecimal, and binary.
```antlr
integer-literal:
: ...
| binary-integer-literal
integer_literal
: decimal_integer_literal
| hexadecimal_integer_literal
| binary_integer_literal
;
decimal_integer_literal
: decimal_digit+ integer_type_suffix?
;
binary-integer-literal:
: `0b` binary-digits integer-type-suffix-opt
| `0B` binary-digits integer-type-suffix-opt
decimal_digit
: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
;
binary-digits:
: binary-digit
| binary-digits binary-digit
integer_type_suffix
: 'U' | 'u' | 'L' | 'l' | 'UL' | 'Ul' | 'uL' | 'ul' | 'LU' | 'Lu' | 'lU' | 'lu'
;
binary-digit:
: `0`
| `1`
hexadecimal_integer_literal
: '0x' hex_digit+ integer_type_suffix?
| '0X' hex_digit+ integer_type_suffix?
;
hex_digit
: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
| 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'a' | 'b' | 'c' | 'd' | 'e' | 'f';
binary_integer_literal
: '0b' binary_digit+ integer_type_suffix?
| '0B' binary_digit+ integer_type_suffix?
;
binary_digit
: '0'
| '1'
;
```
The type of an integer literal is determined as follows:
* If the literal has no suffix, it has the first of these types in which its value can be represented: `int`, `uint`, `long`, `ulong`.
* If the literal is suffixed by `U` or `u`, it has the first of these types in which its value can be represented: `uint`, `ulong`.
* If the literal is suffixed by `L` or `l`, it has the first of these types in which its value can be represented: `long`, `ulong`.
* If the literal is suffixed by `UL`, `Ul`, `uL`, `ul`, `LU`, `Lu`, `lU`, or `lu`, it is of type `ulong`.
If the value represented by an integer literal is outside the range of the `ulong` type, a compile-time error occurs.
As a matter of style, it is suggested that "`L`" be used instead of "`l`" when writing literals of type `long`, since it is easy to confuse the letter "`l`" with the digit "`1`".
To permit the smallest possible `int` and `long` values to be written as decimal integer literals, the following two rules exist:
* When a *decimal_integer_literal* with the value 2147483648 (2^31) and no *integer_type_suffix* appears as the token immediately following a unary minus operator token ([Unary minus operator](expressions.md#unary-minus-operator)), the result is a constant of type `int` with the value -2147483648 (-2^31). In all other situations, such a *decimal_integer_literal* is of type `uint`.
* When a *decimal_integer_literal* with the value 9223372036854775808 (2^63) and no *integer_type_suffix* or the *integer_type_suffix* `L` or `l` appears as the token immediately following a unary minus operator token ([Unary minus operator](expressions.md#unary-minus-operator)), the result is a constant of type `long` with the value -9223372036854775808 (-2^63). In all other situations, such a *decimal_integer_literal* is of type `ulong`.

View file

@ -1,21 +1,181 @@
# Digit separators
Being able to group digits in large numeric literals would have great readability impact and no significant downside.
This proposal specifies the changes required to the [C# 6.0 (draft) Language specification](../../spec/introduction.md) to support *literal digit separators*.
Adding binary literals (#215) would increase the likelihood of numeric literals being long, so the two features enhance each other.
## Changes to [Lexical structure](../../spec/lexical-structure.md)
We would follow Java and others, and use an underscore `_` as a digit separator. It would be able to occur everywhere in a numeric literal (except as the first and last character), since different groupings may make sense in different scenarios and especially for different numeric bases:
### Literals
```csharp
int bin = 0b1001_1010_0001_0100;
int hex = 0x1b_a0_44_fe;
int dec = 33_554_432;
int weird = 1_2__3___4____5_____6______7_______8________9;
double real = 1_000.111_1e-1_000;
#### Integer literals
> The grammar for [integer literals](../../spec/lexical-structure.md#Integer-literals) is modified to allow one or more `_` separators anywhere between the first and last digits.
Integer literals are used to write values of types `int`, `uint`, `long`, and `ulong`. Integer literals have three possible forms: decimal, hexadecimal, and binary.
```antlr
integer_literal
: decimal_integer_literal
| hexadecimal_integer_literal
| binary_integer_literal
;
integer_type_suffix
: 'U' | 'u' | 'L' | 'l' | 'UL' | 'Ul' | 'uL' | 'ul' | 'LU' | 'Lu' | 'lU' | 'lu'
;
decimal_integer_literal
: decimal_digits integer_type_suffix?
;
decimal_digits
: decimal_digit
| decimal_digit decimal_digits_and_underscores? decimal_digit
;
decimal_digit
: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
;
decimal_digits_and_underscores
: decimal_digit_or_underscore+
;
decimal_digit_or_underscore
: decimal_digit
| '_'
;
hexadecimal_integer_literal
: '0x' hex_digits integer_type_suffix?
| '0X' hex_digits integer_type_suffix?
;
hex_digits
: hex_digit
| hex_digit hex_digits_and_underscores? hex_digit
;
hex_digit
: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
| 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'a' | 'b' | 'c' | 'd' | 'e' | 'f'
;
hex_digits_and_underscores
: hex_digit_or_underscore+
;
hex_digit_or_underscore
: hex_digit
| '_'
;
binary_integer_literal
: '0b' binary_digits integer_type_suffix?
| '0B' binary_digits integer_type_suffix?
;
binary_digits
: binary_digit
| binary_digit binary_digits_and_underscores? binary_digit
;
binary_digit
: '0'
| '1'
;
binary_digits_and_underscores
: binary_digit_or_underscore+
;
binary_digit_or_underscore
: binary_digit
| '_'
;
```
Any sequence of digits may be separated by underscores, possibly more than one underscore between two consecutive digits. They are allowed in decimals as well as exponents, but following the previous rule, they may not appear next to the decimal (`10_.0`), next to the exponent character (`1.1e_1`), or next to the type specifier (`10_f`). When used in binary and hexadecimal literals, they may not appear immediately following the `0x` or `0b`.
The type of an integer literal is determined as follows:
The syntax is straightforward, and the separators have no semantic impact - they are simply ignored.
* If the literal has no suffix, it has the first of these types in which its value can be represented: `int`, `uint`, `long`, `ulong`.
* If the literal is suffixed by `U` or `u`, it has the first of these types in which its value can be represented: `uint`, `ulong`.
* If the literal is suffixed by `L` or `l`, it has the first of these types in which its value can be represented: `long`, `ulong`.
* If the literal is suffixed by `UL`, `Ul`, `uL`, `ul`, `LU`, `Lu`, `lU`, or `lu`, it is of type `ulong`.
This has broad value and is easy to implement.
If the value represented by an integer literal is outside the range of the `ulong` type, a compile-time error occurs.
As a matter of style, it is suggested that "`L`" be used instead of "`l`" when writing literals of type `long`, since it is easy to confuse the letter "`l`" with the digit "`1`".
To permit the smallest possible `int` and `long` values to be written as decimal integer literals, the following two rules exist:
* When a *decimal_integer_literal* with the value 2147483648 (2^31) and no *integer_type_suffix* appears as the token immediately following a unary minus operator token ([Unary minus operator](expressions.md#unary-minus-operator)), the result is a constant of type `int` with the value -2147483648 (-2^31). In all other situations, such a *decimal_integer_literal* is of type `uint`.
* When a *decimal_integer_literal* with the value 9223372036854775808 (2^63) and no *integer_type_suffix* or the *integer_type_suffix* `L` or `l` appears as the token immediately following a unary minus operator token ([Unary minus operator](expressions.md#unary-minus-operator)), the result is a constant of type `long` with the value -9223372036854775808 (-2^63). In all other situations, such a *decimal_integer_literal* is of type `ulong`.
\[Example:
```csharp
123 // decimal, int
10_543_765Lu // decimal, ulong
1_2__3___4____5 // decimal, int
0xFf // hex, int
0X1b_a0_44_fEL // hex, long
0x1ade_3FE1_29AaUL // hex, ulong
0xabc_ // invalid; no trailing _ allowed
0b101 // binary, int
0B1001_1010u // binary, uint
0b1111_1111_0000UL // binary, ulong
0B__111 // invalid; no leading _ allowed
```
end example\]
#### Real literals
> The grammar for [real literals](../../spec/lexical-structure.md#Real-literals) is modified to allow `_` separators within decimals, fractions, and exponents, anywhere between the first and last digits.
Real literals are used to write values of types `float`, `double`, and `decimal`.
```antlr
real_literal
: decimal_digits '.' decimal_digits exponent_part? real_type_suffix?
| '.' decimal_digits exponent_part? real_type_suffix?
| decimal_digits exponent_part real_type_suffix?
| decimal_digits real_type_suffix
;
exponent_part
: 'e' sign? decimal_digits
| 'E' sign? decimal_digits
;
sign
: '+'
| '-'
;
real_type_suffix
: 'F' | 'f' | 'D' | 'd' | 'M' | 'm'
;
```
If no *real_type_suffix* is specified, the type of the real literal is `double`. Otherwise, the real type suffix determines the type of the real literal, as follows:
* A real literal suffixed by `F` or `f` is of type `float`. For example, the literals `1f`, `1.5f`, `1e10f`, and `123.456F` are all of type `float`.
* A real literal suffixed by `D` or `d` is of type `double`. For example, the literals `1d`, `1.5d`, `1e10d`, and `123.456D` are all of type `double`.
* A real literal suffixed by `M` or `m` is of type `decimal`. For example, the literals `1m`, `1.5m`, `1e10m`, and `123.456M` are all of type `decimal`. This literal is converted to a `decimal` value by taking the exact value, and, if necessary, rounding to the nearest representable value using banker's rounding ([The decimal type](types.md#the-decimal-type)). Any scale apparent in the literal is preserved unless the value is rounded or the value is zero (in which latter case the sign and scale will be 0). Hence, the literal `2.900m` will be parsed to form the decimal with sign `0`, coefficient `2900`, and scale `3`.
If the specified literal cannot be represented in the indicated type, a compile-time error occurs.
The value of a real literal of type `float` or `double` is determined by using the IEEE "round to nearest" mode.
\[Example:
```csharp
1.234_567 // double
.3e5f // float
2_345E-2_0 // double
15D // double
19.73M // decimal
1.F // invalid; ill-formed (parsed as "1." and "F")
1.234_ // invalid; no trailing _ allowed in fraction
.3e5_F // invalid; no trailing _ allowed in exponent
```
end example\]

View file

@ -1,3 +1,632 @@
## Tuples
# Tuples
In C# 7.0 we added support for *tuples*. This is a placeholder for its specification.
This proposal specifies the changes required to the [C# 6.0 (draft) Language specification](../../spec/introduction.md) to support *Tuples* as a new [value type](../../spec/types.md#value-types).
## Additions to [Expressions](../../spec/expressions.md)
> Add the following section after [xxx](xxx):
## Tuple literal expressions
A tuple literal consists of two or more tuple literal elements, each of which is optionally named.
```antlr
tuple_literal
: '(' ( tuple_literal_element ',' )+ tuple_literal_element ')'
;
tuple_literal_element
: ( identifier ':' )? expression
;
```
A tuple literal is implicitly typed; that is, its type is determined by the context in which it is used, referred to as the *target*. Each element *expression* in a tuple literal shall have a value that can be converted implicitly to its corresponding target element type.
\[Example:
```csharp
var t1 = (0, 2); // infer tuple type (int, int) from values
var t2 = (sum: 0, count: 1); // infer tuple type (int sum, int count) from names and values
(int, double) t3 = (0, 2); // infer tuple type (int, double) from values; can implicitly convert int to double
(int, double) t4 = (0.0, 2); // Error: can't implicitly convert double to int
```
end example\]
A tuple literal has a "conversion from expression" to any tuple type of the same arity, as long as each of the element expressions of the tuple literal has an implicit conversion to the type of the corresponding element of the tuple type.
\[Example:
```csharp
(string name, byte age) t = (null, 5); // OK: null and 5 convert to string and byte, respectively
```
end example\]
In cases where a tuple literal is not part of a conversion, the literal's type is its [natural type](XXX), if one exists.
\[Example:
```csharp
var t = ("John", 5); // OK: the natural type is (string, int)
var t = (null, 5); // Error: null doesn't have a type
var t = (name: "John", age: 5); // OK: The natural type is (string name, int age)
```
end example\]
A tuple literal is *not* a [constant expression](../../spec/expressions.md#Constant-expressions).
For a discussion of tuple literals as tuple initializers, see [Tuple types](XXX).
## Additions to [Types](../../spec/types.md)
> Add the following sections after [Nullable types](../../spec/types.md#nullable-types) (at the end of the current *Value types* section.)
### Tuple types
#### General
A tuple is declared using the following syntax:
```antlr
tuple_type
: '(' tuple_type_element_list ')'
;
tuple_type_element_list
: tuple_type_element ',' tuple_type_element
| tuple_type_element_list ',' tuple_type_element
;
tuple_type_element
: type identifier?
;
```
A ***tuple*** is an anonymous data structure type that contains an ordered sequence of two or more ***elements***, which are optionally named. Each element is public. If a tuple is mutable, its element values are also mutable?
A tuple's ***natural type*** is the combination of its element types, in lexical order, and element names, if they exist.
A tuple's ***arity*** is the combination of its element types, in lexical order; element names are *not* included.
Each unique tuple arity designates a distinct tuple type.
Two tuple values are equal if they have the same arity, and the values of the elements in each corresponding element pair are equal.
An element in a tuple is accessed using the [member-access operator `.`](../../spec/expressions.md#Member-access).
Given the following,
```csharp
(int code, string message) pair1 = (3, "hello");
System.Console.WriteLine("first = {0}, second = {1}", pair1.code, pair1.message);
```
the syntax `(int code, string message)` declares a tuple type having two elements, each with the given type and name.
As shown, a tuple can be initialized using a [tuple literal](XXX).
An element need not have a name. An element without a name is unnamed.
If a tuple declarator contains the type of all the tuple's elements, that set of types cannot be changed or augmented based on the context in which it is used; otherwise, element type information shall be inferred from the usage context. Likewise for element names.
A tuple's type can be declared explicitly. Consider the following declarations:
```csharp
(int, string) pair2 = (2, "Goodbye");
(int code, string message) pair3 = (2, "Goodbye");
(int code, string) pair4 = (2, "Goodbye");
(int, string message) pair5 = (2, "Goodbye");
(int code, string) pair6 = (2, message: "Goodbye"); // Warning: can't give a name to the second element
(int code, string) pair7 = (newName: 2, "Goodbye"); // Warning: can't change the name of element code
```
The type of `pair2` is `(int, string)` with unknown element names. Similarly, the type of `pair3` is `(int, string)` but this time with the element names `code` and `message`, respectively. For `pair4` and `pair5`, one element is named, the other not.
In the case of `pair6`, the second element is declared as being unnamed, and any attempt to provide a name in an initializing context shall be ignored. Likewise for any attempt to change an element's name, as in the case of `pair7`.
A tuple's type can be wholely inferred from the context in which it is used. Consider the following declarations:
```csharp
var pair10 = (1, "Hello");
var pair11 = (code: 1, message: "Hello");
var pair12 = (code: 1, "Hello");
var pair13 = (1, message: "Hello");
```
The type of `pair10` is inferred from the initializer's tuple literal, as `(int, string)` with unknown element names. Similarly, the type of `pair11` is inferred from the initialer's tuple literal, as `(int, string)` but this time with the element names `code` and `message`, respectively. For `pair12` and `pair13`, the element types are inferred, and one element is named, the other not.
Element names within a tuple type shall be distinct.
\[Example:
```csharp
(int e1, float e1) t = (10, 1.2); // Error: both elements have the same name
(int e1, (int e1, int e2) e2) t = (10, (20, 30)); // OK: element names in each tuple type are distinct
```
end example\]
The name of any element in a partial type declaration shall be the same for an element in the same position in any other partial declaration for that type.
\[Example:
```csharp
partial class C : IEnumerable<(string name, int age)> { ... }
partial class C : IEnumerable<(string fullname, int)> { ... } // Error: names must be specified and the same
```
end example\]
A tuple cannot be created with the `new` operator. However, the `new` operator can be used to create and initialize an array of tuple or a nullable tuple.
#### A tuple's underlying type
Each tuple type maps to an underlying type. Specifically, a tuple having two elements maps to `System.ValueTuple<T1, T2>`, one with three elements maps to `System.ValueTuple<T1, T2, T3>`, and so on, up to seven elements. Tuple types having eight or more elements map to `System.ValueTuple<T1, T2, T3,..., T7, TRest>`. The first element in an underlying type has the public name `Item1`, the second `Item2`, and so on through `Item7`. Any elements beyond seven can be accesed as a group by the public name `Rest`, whose type is "tuple of the remaining elements". Alternatively, those elements can be accessed individually using the names `Item8` through `Item`*N*, where *N* is the total number of elements, even though the underlying type has no such names defined.
A tuple type shall behave exactly like its underlying type. The only additional enhancement in the tuple type case is the ability to provide a more expressive name for each element.
\[Example:
```csharp
var t1 = (sum: 0, 1);
t1.sum = 1; // access the first element by its declared name
t1.Item1 = 1; // access the first element by its underlying name
t1.Item2 = 3; // access the second element by its underlying name
System.ValueTuple<int, int> vt = t1; // identity conversion
var t2 = (1, 2, 3, 4, 5, 6, 7, 8, 9); // t2 is a System.ValueTuple<T1, T2, T3,..., T7, TRest>
var t3 = t4.Rest; // t3 is a (int, int); that is, a System.ValueTuple<T1, T2>
System.Console.WriteLine("Item9 = {0}", t1.Item9); // outputs 9 even though no such name Item9 exists!
```
end example\]
\[Example:
```csharp
var t = (ToString: 0, GetHashCode: 1); // Error: names match underlying member names
var t1 = (Item1: 0, Item2: 1); // OK
var t2 = (misc: 0, Item1: 1); // Error: "Item1" used in a wrong position
```
end example\]
#### Element names and overloading, overriding, and hiding
When tuple element names are used in overridden signatures or implementations of interface methods, tuple element names in parameter and return types shall be preserved. It is an error for the same generic interface to be inherited or implemented twice with identity-convertible type arguments that have conflicting tuple element names.
For the purpose of overloading, overriding and hiding, tuples of the same arity, as well as their underlying ValueTuple types, shall be considered equivalent. All other differences are immaterial. When overriding a member it shall be permitted to use tuple types with the same element names or element names different than in the base member.
If the same element name is used for non-matching elements in base and derived member signatures, the implementation shall issue a warning.
```csharp
public class Base
{
public virtual void M1(ValueTuple<int, int> arg){...}
}
public class Derived : Base
{
public override void M1((int c, int d) arg){...} // valid override, signatures are equivalent
}
public class Derived2 : Derived
{
public override void M1((int c1, int c) arg){...} // also valid, warning on possible misuse of name 'c'
}
public class InvalidOverloading
{
public virtual void M1((int c, int d) arg){...}
public virtual void M1((int x, int y) arg){...} // invalid overload, signatures are eqivalent
public virtual void M1(ValueTuple<int, int> arg){...} // also invalid
}
```
### Tuple element name erasure at runtime
A tuple element name is not part of the runtime representation of a tuple of that type; an element's name is tracked only by the compiler. [*Note*: As a result, element names are not available to a third-party observer of a tuple instance (such as with reflection or dynamic code). *end note*]
In alignment with the identity conversions, a boxed tuple shall not retain the names of the elements, and shall unbox to any tuple type that has the same element types in the same order.
\[Example:
```csharp
object o = (a: 1, b: 2); // boxing conversion
var t = ((int moo, int boo))o; // unboxing conversion
```
end example\]
## Additions to [Variables](../../spec/variables.md)
> Add the following text at the end of the [Variables](../../spec/variables.md) section.
## Discards
The identifier `_` can be used as a *discard* in the following circumstances:
- When no identifier `_` is defined in the current scope.
- A "designator" `var _` or `T _` in [deconstruction](XXX), [pattern matching](XXX) and [out vars](XXX).
Like unassigned variables, discards do not have a value. A discard may only occur in contexts where it is assigned to.
\[Example:
```csharp
M(out _, out var _, out int _); // three out variable discards
(_, var _, int _) = GetCoordinates(); // deconstruction into discards
if (x is var _ && y is int _) { ... } // discards in patterns
```
end example\]
## Changes to [Conversions](../spec/conversions.md)
### Identity conversion
> Add the following text to [Identity conversion](../../spec/conversions.md#identity-conversion), after the bullet point on `object` and `dynamic`:
* Element names are immaterial to tuple conversions. Tuples with the same arity are identity-convertible to each other or to and from corresponding underlying `ValueTuple` types, regardless of their element names.
\[Example:
```csharp
var t = (sum: 0, count: 1);
System.ValueTuple<int, int> vt = t; // identity conversion
(int moo, int boo) t2 = vt; // identity conversion
t2.moo = 1;
```
end example\]
In the case in which an element name at one position on one side of a conversion, and the same name at a different position on the other side, the compiler shall issue a warning.
\[Example:
```csharp
(string first, string last) GetNames() { ... }
(string last, string first) names = GetNames(); // Oops!
```
end example\]
### Boxing conversions
> Add the following text to [Boxing conversions](../../spec/conversions.md#boxing-conversions) after the first paragraph:
Tuples have a boxing conversion. Importantly, the element names aren't part of the runtime representation of tuples, but are tracked only by the compiler. Thus, once element names have been "cast away", they cannot be recovered. In alignment with identity conversion, a boxed tuple unboxes to any tuple type that has the same arity.
### Tuple conversions
> Add this section after [Implicit enumeration conversions](../../spec/conversions.md#implicit-enumeration-conversions)
Tuple types and expressions support a variety of conversions by "lifting" conversions of the elements into overall *tuple conversion*. For the classification purpose, all element conversions are considered recursively. For example, to have an implicit conversion, all element expressions/types shall have implicit conversions to the corresponding element types.
Tuple conversions are *Standard Conversions*.
An implicit tuple conversion is a standard conversion. It applies from one tuple type to another of equal arity when here is any implicit conversion from each element in the source tuple to the corresponding element in the destination tuple.
An explicit tuple conversion is a standard conversion. It applies between two tuple types of equal arity when there is any explicit conversion between each corresponding pair of element types.
A tuple conversion can be classified as a valid instance conversion or an extension method invocation as long as all element conversions are applicable as instance conversions.
On top of the member-wise conversions implied by implicit typing, implicit conversions between tuple types themselves are allowed.
### Tuple Literal Conversion
> Add this section after [Anonymous function conversions and method group conversions](../../spec.md#Anonymous-function-conversions-and-method-group-conversions)
A tuple literal is implicitly typed when used in a context specifying a tuple type. The tuple literal has a "conversion from expression" to any tuple type of the same arity, as long as the element expressions of the tuple literal have an implicit conversion to the corresponding element types of the tuple type.
\[Example:
```csharp
(string name, byte age) t = (null, 5); // OK: the expressions null and 5 convert to string and byte
```
end example\]
A successful conversion from tuple expression to tuple type is classified as an *ImplicitTuple* conversion, unless the tuple's [natural type](XXX) matches the target type exactly, in such case it is an *Identity* conversion.
```csharp
void M1((int x, int y) arg){...};
void M1((object x, object y) arg){...};
M1((1, 2)); // first overload is used. Identity conversion is better than implicit conversion.
M1(("hi", "hello")); // second overload is used. Implicit tuple conversion is better than no conversion.
```
A successful conversion from tuple expression to a nullable tuple type is classified as *ImplicitNullable* conversion.
```csharp
((int x, int y, int z)?, int t)? SpaceTime()
{
return ((1,2,3), 7); // valid, implicit nullable conversion
}
```
## Additions to [Expressions](../../spec/expressions.md)
### Overload resolution and tuples with no natural types
> Add the following text after the bullet list in [Exactly matching expressions](../../spec/expressions.md#Exactly-matching-expressions):
The exact-match rule for tuple expressions is based on the [natural types](XXX) of the constituent tuple elements. The rule is mutually recursive with respect to other containing or contained expressions not in a possession of a natural type.
### Deconstruction expressions
> Add this section at the end of the [Expressions](../../spec/expressions.md) chapter.
A tuple-deconstruction expression copies from a source tuple zero or more of its element values to corresponding destinations.
```antlr
tuple_deconstruction_expression
: '(' destination_list ')'
;
destination_list
: destination ',' destination
| destination_list ',' destination
;
destination
: type identifier
;
```
Element values are copied from the source tuple to the destination(s). Each element's position is inferred from the destination position within *destination_list*. A destination with identifier `_` indicates that the corresponding element is discarded rather than being copied. The destination list shall account for every element in the tuple.
\[Example:
```csharp
int code;
string message;
(code, message) = (10, "hello"); // copy both element values to existing variables
(code, _) = (11, "Go!"); // copy element 1 to code and discard element 2
(_, _) = (12, "Stop!"); // discard both element values
(int code2, string message2) = (20, "left"); // copy both element values to newly created variables
(code, string message3) = (21, "right"); // Error: can't mix existing and new variables
(code, _) = (30, 2.5, (10, 20)); // Error: can't deconstruct tuple of 3 elements into 2 values
(code, _, _) = (30, 2.5, (10, 20)); // OK: deconstructing 3 elements into 3 values
```
end example\]
Any object may be deconstructed by providing an accessible `Deconstruct` method, either as an instance member or as an extension method. A `Deconstruct` method converts an object to a set of discrete values. The Deconstruct method "returns" the component values by use of individual `out` parameters. `Deconstruct` is overloadable. Consider the following:
```csharp
class Name
{
public void Deconstruct(out string first, out string last) {
first = First; last = Last;
}
...
}
// or
static class Extensions
{
public static void Deconstruct(this Name name, out string first, out string last) {
first = name.First; last = name.Last;
}
}
```
Overload resolution for `Deconstruct` methods considers only the arity of the `Deconstruct` method. If multiple `Deconstruct` methods of the same arity are accessible, the expression is ambiguous and a binding-time error shall occur.
If necessary to satisfy implicit conversions of the tuple member types, the compiler passes temporary variables to the `Deconstruct` method, instead of the ones declared in the deconstruction. For example, if object `p` has the following method:
```csharp
void Deconstruct(out byte x, out byte y) ...;
```
the compiler translates
```csharp
(int x, int y) = p;
```
to:
```csharp
p.Deconstruct(out byte __x, out byte __y);
(int x, int y) = (__x, __y);
```
The evaluation order of deconstruction assignment expressions is "breadth first":
1. Evaluate the LHS: Evaluate each of the expressions inside of it one by one, left to right, to yield side effects and establish a storage location for each.
1. Evaluate the RHS: Evaluate each of the expressions inside of it one by one, left to right to yield side effects
1. Convert each of the RHS expressions to the LHS types expected, one by one, left to right.
1. Assign each of the conversion results from Step 3 to the storage locations found in (???)
\[Example:
```csharp
string x;
byte y;
(x, y) = (y, x); // swap!
```
end example\]
A deconstructing assignment is a *statement-expression* whose type could be `void`.
### Additions to [Classes](../../spec/classes.md)
### Extension methods
> Add the following note to the end of the section on [extension methods](../../spec/classes.md#extension-methods):
[*Note*: Extension methods on a tuple type apply to tuples with different element names:
```csharp
static void M(this (int x, int y) t) { ... }
(int a, int b) t = ...;
t.M(); // OK
```
The extension method `M` is a candidate method, even though the tuple `t` has different element names (`a` and `b`) than the formal parameter of `M` (`x` and `y`).
*endnote*].
### Additions to [Annex C: Standard Library](XXX)
> The published standard contains an Annex C, which states, "A conforming C# implementation shall provide a minimum set of types having specific semantics. These types and their members are listed here, in alphabetical order by namespace and type. For a formal definition of these types and their members, refer to ISO/IEC 23271:2012 Common Language Infrastructure (CLI), Partition IV; Base Class Library (BCL), Extended Numerics Library, and Extended Array Library, which are included by reference in this specification."
> The GitHub-based spec does *not* appear to have this annex.
> Add the following section to either of "C.2 Standard Library Types defined in ISO/IEC 23271" or "C.3 Standard Library Types not defined in ISO/IEC 23271:2012", as appropriate:
```csharp
namespace System
{
public struct ValueTuple<[NullableAttribute(2)] T1, [NullableAttribute(2)] T2> : IStructuralComparable,
IStructuralEquatable, IComparable, IComparable<(T1, T2)>, IEquatable<(T1, T2)>, ITuple
{
[NullableAttribute(1)]
public T1 Item1;
[NullableAttribute(1)]
public T2 Item2;
[NullableContextAttribute(1)]
public ValueTuple(T1 item1, T2 item2);
public int CompareTo([NullableAttribute(new[] { 0, 1, 1 })] (T1, T2) other);
[NullableContextAttribute(2)]
public override bool Equals(object? obj);
public bool Equals([NullableAttribute(new[] { 0, 1, 1 })] (T1, T2) other);
public override int GetHashCode();
[NullableContextAttribute(1)]
public override string ToString();
}
}
namespace System
{
public struct ValueTuple<[NullableAttribute(2)] T1, [NullableAttribute(2)] T2, [NullableAttribute(2)] T3>
: IStructuralComparable, IStructuralEquatable, IComparable, IComparable<(T1, T2, T3)>,
IEquatable<(T1, T2, T3)>, ITuple
{
[NullableAttribute(1)]
public T1 Item1;
[NullableAttribute(1)]
public T2 Item2;
[NullableAttribute(1)]
public T3 Item3;
[NullableContextAttribute(1)]
public ValueTuple(T1 item1, T2 item2, T3 item3);
public int CompareTo([NullableAttribute(new[] { 0, 1, 1, 1 })] (T1, T2, T3) other);
[NullableContextAttribute(2)]
public override bool Equals(object? obj);
public bool Equals([NullableAttribute(new[] { 0, 1, 1, 1 })] (T1, T2, T3) other);
public override int GetHashCode();
[NullableContextAttribute(1)]
public override string ToString();
}
}
namespace System
{
[NullableAttribute(0)]
[NullableContextAttribute(1)]
public struct ValueTuple<[NullableAttribute(2)] T1, [NullableAttribute(2)] T2, [NullableAttribute(2)] T3,
[NullableAttribute(2)] T4> : IStructuralComparable, IStructuralEquatable, IComparable,
IComparable<(T1, T2, T3, T4)>, IEquatable<(T1, T2, T3, T4)>, ITuple
{
public T1 Item1;
public T2 Item2;
public T3 Item3;
public T4 Item4;
public ValueTuple(T1 item1, T2 item2, T3 item3, T4 item4);
public int CompareTo([NullableAttribute(new[] { 0, 1, 1, 1, 1 })] (T1, T2, T3, T4) other);
[NullableContextAttribute(2)]
public override bool Equals(object? obj);
public bool Equals([NullableAttribute(new[] { 0, 1, 1, 1, 1 })] (T1, T2, T3, T4) other);
public override int GetHashCode();
public override string ToString();
}
}
namespace System
{
[NullableAttribute(0)]
[NullableContextAttribute(1)]
public struct ValueTuple<[NullableAttribute(2)] T1, [NullableAttribute(2)] T2, [NullableAttribute(2)] T3,
[NullableAttribute(2)] T4, [NullableAttribute(2)] T5> : IStructuralComparable, IStructuralEquatable,
IComparable, IComparable<(T1, T2, T3, T4, T5)>, IEquatable<(T1, T2, T3, T4, T5)>, ITuple
{
public T1 Item1;
public T2 Item2;
public T3 Item3;
public T4 Item4;
public T5 Item5;
public ValueTuple(T1 item1, T2 item2, T3 item3, T4 item4, T5 item5);
public int CompareTo([NullableAttribute(new[] { 0, 1, 1, 1, 1, 1 })] (T1, T2, T3, T4, T5) other);
[NullableContextAttribute(2)]
public override bool Equals(object? obj);
public bool Equals([NullableAttribute(new[] { 0, 1, 1, 1, 1, 1 })] (T1, T2, T3, T4, T5) other);
public override int GetHashCode();
public override string ToString();
}
}
namespace System
{
[NullableAttribute(0)]
[NullableContextAttribute(1)]
public struct ValueTuple<[NullableAttribute(2)] T1, [NullableAttribute(2)] T2, [NullableAttribute(2)] T3,
[NullableAttribute(2)] T4, [NullableAttribute(2)] T5, [NullableAttribute(2)] T6> : IStructuralComparable,
IStructuralEquatable, IComparable, IComparable<(T1, T2, T3, T4, T5, T6)>,
IEquatable<(T1, T2, T3, T4, T5, T6)>, ITuple
{
public T1 Item1;
public T2 Item2;
public T3 Item3;
public T4 Item4;
public T5 Item5;
public T6 Item6;
public ValueTuple(T1 item1, T2 item2, T3 item3, T4 item4, T5 item5, T6 item6);
public int CompareTo([NullableAttribute(new[] { 0, 1, 1, 1, 1, 1, 1 })] (T1, T2, T3, T4, T5, T6) other);
[NullableContextAttribute(2)]
public override bool Equals(object? obj);
public bool Equals([NullableAttribute(new[] { 0, 1, 1, 1, 1, 1, 1 })] (T1, T2, T3, T4, T5, T6) other);
public override int GetHashCode();
public override string ToString();
}
}
namespace System
{
[NullableAttribute(0)]
[NullableContextAttribute(1)]
public struct ValueTuple<[NullableAttribute(2)] T1, [NullableAttribute(2)] T2, [NullableAttribute(2)] T3,
[NullableAttribute(2)] T4, [NullableAttribute(2)] T5, [NullableAttribute(2)] T6, [NullableAttribute(2)] T7>
: IStructuralComparable, IStructuralEquatable, IComparable, IComparable<(T1, T2, T3, T4, T5, T6, T7)>,
IEquatable<(T1, T2, T3, T4, T5, T6, T7)>, ITuple
{
public T1 Item1;
public T2 Item2;
public T3 Item3;
public T4 Item4;
public T5 Item5;
public T6 Item6;
public T7 Item7;
public ValueTuple(T1 item1, T2 item2, T3 item3, T4 item4, T5 item5, T6 item6, T7 item7);
public int CompareTo([NullableAttribute(new[] { 0, 1, 1, 1, 1, 1, 1, 1 })]
(T1, T2, T3, T4, T5, T6, T7) other);
[NullableContextAttribute(2)]
public override bool Equals(object? obj);
public bool Equals([NullableAttribute(new[] { 0, 1, 1, 1, 1, 1, 1, 1 })]
(T1, T2, T3, T4, T5, T6, T7) other);
public override int GetHashCode();
public override string ToString();
}
}
namespace System
{
[NullableAttribute(0)]
[NullableContextAttribute(1)]
public struct ValueTuple<[NullableAttribute(2)] T1, [NullableAttribute(2)] T2, [NullableAttribute(2)] T3,
[NullableAttribute(2)] T4, [NullableAttribute(2)] T5, [NullableAttribute(2)] T6, [NullableAttribute(2)] T7,
[NullableAttribute(0)] TRest> : IStructuralComparable, IStructuralEquatable, IComparable,
IComparable<ValueTuple<T1, T2, T3, T4, T5, T6, T7, TRest>>,
IEquatable<ValueTuple<T1, T2, T3, T4, T5, T6, T7, TRest>>, ITuple where TRest : struct
{
public T1 Item1;
public T2 Item2;
public T3 Item3;
public T4 Item4;
public T5 Item5;
public T6 Item6;
public T7 Item7;
[NullableAttribute(0)]
public TRest Rest;
public ValueTuple(T1 item1, T2 item2, T3 item3, T4 item4, T5 item5, T6 item6, T7 item7,
[NullableAttribute(0)] TRest rest);
public int CompareTo([NullableAttribute(new[] { 0, 1, 1, 1, 1, 1, 1, 1, 0 })]
ValueTuple<T1, T2, T3, T4, T5, T6, T7, TRest> other);
[NullableContextAttribute(2)]
public override bool Equals(object? obj);
public bool Equals([NullableAttribute(new[] { 0, 1, 1, 1, 1, 1, 1, 1, 0 })]
ValueTuple<T1, T2, T3, T4, T5, T6, T7, TRest> other);
public override int GetHashCode();
public override string ToString();
}
}
```

View file

@ -1,17 +1,32 @@
# Allow digit separator after 0b or 0x
In C# 7.2, we extend the set of places that digit separators (the underscore character) can appear in integral literals. [Beginning in C# 7.0, separators are permitted between the digits of a literal](../csharp-7.0/digit-separators.md). Now, in C# 7.2, we also permit digit separators before the first significant digit of a binary or hexadecimal literal, after the prefix.
This proposal specifies the changes required to the [C# 7.1 (draft) Language specification](XXX) to support leading underscores in binary and hex literals. It builds on top of the changes added in 7.0 when binary literals were introduced.
```csharp
123 // permitted in C# 1.0 and later
1_2_3 // permitted in C# 7.0 and later
0x1_2_3 // permitted in C# 7.0 and later
0b101 // binary literals added in C# 7.0
0b1_0_1 // permitted in C# 7.0 and later
## Changes to [Lexical structure](../../spec/lexical-structure.md)
// in C# 7.2, _ is permitted after the `0x` or `0b`
0x_1_2 // permitted in C# 7.2 and later
0b_1_0_1 // permitted in C# 7.2 and later
### Literals
#### Integer literals
> The grammar for [integer literals](../../spec/lexical-structure.md#Integer-literals) is modified to allow one or more `_` separators before the first digit of a hex or binary literal.
```antlr
hex_digits
: '_'? hex_digit
| '_'? hex_digit hex_digits_and_underscores? hex_digit
;
binary_digits
: '_'? binary_digit
| '_'? binary_digit binary_digits_and_underscores? binary_digit
;
```
We do not permit a decimal integer literal to have a leading underscore. A token such as `_123` is an identifier.
> Make the following changes to the examples:
\[Example:
```csharp
0x_abc // hex, int
0B__111 // binary, int
```
end example\]

View file

@ -1,54 +1,59 @@
# Support for == and != on tuple types
Allow expressions `t1 == t2` where `t1` and `t2` are tuple or nullable tuple types of same cardinality, and evaluate them roughly as `temp1.Item1 == temp2.Item1 && temp1.Item2 == temp2.Item2` (assuming `var temp1 = t1; var temp2 = t2;`).
This proposal specifies the changes required to the [C# 7.2 (draft) Language specification](../../spec/introduction.md) to support equality and inequality operations on tuples.
Conversely it would allow `t1 != t2` and evaluate it as `temp1.Item1 != temp2.Item1 || temp1.Item2 != temp2.Item2`.
## Relational and type-testing operators
In the nullable case, additional checks for `temp1.HasValue` and `temp2.HasValue` are used. For instance, `nullableT1 == nullableT2` evaluates as `temp1.HasValue == temp2.HasValue ? (temp1.HasValue ? ... : true) : false`.
...
When an element-wise comparison returns a non-bool result (for instance, when a non-bool user-defined `operator ==` or `operator !=` is used, or in a dynamic comparison), then that result will be either converted to `bool` or run through `operator true` or `operator false` to get a `bool`. The tuple comparison always ends up returning a `bool`.
> Add the following section after [Enumeration comparison operators](../../spec/expressions.md#enumeration-comparison-operators):
As of C# 7.2, such code produces an error (`error CS0019: Operator '==' cannot be applied to operands of type '(...)' and '(...)'`), unless there is a user-defined `operator==`.
### Tuple comparison operators
## Details
The predefined tuple equality operators are:
When binding the `==` (or `!=`) operator, the existing rules are: (1) dynamic case, (2) overload resolution, and (3) fail.
This proposal adds a tuple case between (1) and (2): if both operands of a comparison operator are tuples (have tuple types or are tuple literals) and have matching cardinality, then the comparison is performed element-wise. This tuple equality is also lifted onto nullable tuples.
```csharp
bool operator ==(Tup1 t1, Tup2 t2);
bool operator !=(Tup1 t1, Tup2 t2);
```
Both operands (and, in the case of tuple literals, their elements) are evaluated in order from left to right. Each pair of elements is then used as operands to bind the operator `==` (or `!=`), recursively. Any elements with compile-time type `dynamic` cause an error. The results of those element-wise comparisons are used as operands in a chain of conditional AND (or OR) operators.
For any tuple or nullable tuple types `Tup1` and `Tup2`, `t1` and `t2` shall have the same number of elements, and operators `==` and `!=` shall be defined for the types of each corresponding element pair. Consider the following:
For instance, in the context of `(int, (int, int)) t1, t2;`, `t1 == (1, (2, 3))` would evaluate as `temp1.Item1 == temp2.Item1 && temp1.Item2.Item1 == temp2.Item2.Item1 && temp1.Item2.Item2 == temp2.Item2.Item2`.
When a tuple literal is used as operand (on either side), it receives a converted tuple type formed by the element-wise conversions which are introduced when binding the operator `==` (or `!=`) element-wise.
The result of `==` is `true` if the values in each corresponding element pair compare equal using the operator `==` for their types. Otherwise, the result is `false`.
For instance, in `(1L, 2, "hello") == (1, 2L, null)`, the converted type for both tuple literals is `(long, long, string)` and the second literal has no natural type.
The result of `!=` is `true` if any comparison of the values in each corresponding element pair compare unequal using the operator `!=` for their types. Otherwise, the result is `false`.
Both operands are evaluated in order from left-to-right. Each pair of elements is then used as operands to bind the operator `==` (or `!=`), recursively. Any elements with compile-time type `dynamic` cause an error.
### Deconstruction and conversions to tuple
In `(a, b) == x`, the fact that `x` can deconstruct into two elements does not play a role. That could conceivably be in a future proposal, although it would raise questions about `x == y` (is this a simple comparison or an element-wise comparison, and if so using what cardinality?).
Similarly, conversions to tuple play no role.
Element names are ignored during tuple comparison.
### Tuple element names
When a tuple literal is used as an operand, it takes on a converted tuple type formed by the element-wise conversions that are introduced when binding the operator `==` (or `!=`) element-wise. For instance, in `(1L, 2, "hello") == (1, 2L, null)`, the converted type for both tuple literals is `(long, long, string)` and the second literal has no natural type.
When converting a tuple literal, we warn when an explicit tuple element name was provided in the literal, but it doesn't match the target tuple element name.
We use the same rule in tuple comparison, so that assuming `(int a, int b) t` we warn on `d` in `t == (c, d: 0)`.
In the nullable tuple case, additional checks for `t1.HasValue` and/or `t2.HasValue` shall be performed.
### Non-bool element-wise comparison results
When an element-wise comparison returns a non-`bool` result, if that comparison is dynamic in a tuple equality, a dynamic invocation of the operator `false` shall be used with the result being negated to get a `bool`.
If an element-wise comparison is dynamic in a tuple equality, we use a dynamic invocation of the operator `false` and negate that to get a `bool` and continue with further element-wise comparisons.
If an element-wise comparison returns some other non-`bool` type in a tuple equality, there are two cases:
If an element-wise comparison returns some other non-bool type in a tuple equality, there are two cases:
- if the non-bool type converts to `bool`, we apply that conversion,
- if there is no such conversion, but the type has an operator `false`, we'll use that and negate the result.
- if the non-bool type converts to `bool`, that conversion is applied,
- if there is no such conversion, but the type has an operator `false`, that is used the result is negated.
In a tuple inequality, the same rules apply except that we'll use the operator `true` (without negation) instead of the operator `false`.
In a tuple inequality, the same rules apply except that the operator `true` is used without negation.
Those rules are similar to the rules involved for using a non-bool type in an `if` statement and some other existing contexts.
When binding the `==` (or `!=`) operator, the usual rules are: (1) dynamic case, (2) overload resolution, and (3) fail. However, in the case of tuple comparison, a new rule is inserted between (1) and (2): if both operands of a comparison operator are tuples, the comparison is performed element-wise. This tuple equality is also lifted onto nullable tuples. If prior to the addition of tuple comparison to C#, a program defined `ValueTuple` types with `==` or `!=` operators, those operators would have been chosen by overload resolution. However, with the addition of comparison support and the new rule above, the comparison is handled by tuple comparison instead of the user-defined comparison.
## Evaluation order and special cases
The left-hand-side value is evaluated first, then the right-hand-side value, then the element-wise comparisons from left to right (including conversions, and with early exit based on existing rules for conditional AND/OR operators).
Regarding the order of evaluation of `t1` and `t2`, `t1` is evaluated first followed by `t2`, then the element-wise comparisons going from left-to-right. Consider the following.
If there is a conversion from type `A` to type `B` and a method `(A, A) GetTuple()`, the comparison
```csharp
(new A(1), (new B(2), new B(3))) == (new B(4), GetTuple())
```
is evaluated thus:
For instance, if there is a conversion from type `A` to type `B` and a method `(A, A) GetTuple()`, evaluating `(new A(1), (new B(2), new B(3))) == (new B(4), GetTuple())` means:
- `new A(1)`
- `new B(2)`
- `new B(3)`
@ -56,22 +61,13 @@ For instance, if there is a conversion from type `A` to type `B` and a method `(
- `GetTuple()`
- then the element-wise conversions and comparisons and conditional logic is evaluated (convert `new A(1)` to type `B`, then compare it with `new B(4)`, and so on).
### Comparing `null` to `null`
This is a special case from regular comparisons, that carries over to tuple comparisons. The `null == null` comparison is allowed, and the `null` literals do not get any type.
In tuple equality, this means, `(0, null) == (0, null)` is also allowed and the `null` and tuple literals don't get a type either.
> Add the following to the end of section [Equality operators and null](../../spec/expressions.md#Equality-operators-and-null):
### Comparing a nullable struct to `null` without `operator==`
### Equality operators and null
This is another special case from regular comparisons, that carries over to tuple comparisons.
If you have a `struct S` without `operator==`, the `(S?)x == null` comparison is allowed, and it is interpreted as `((S?).x).HasValue`.
In tuple equality, the same rule is applied, so `(0, (S?)x) == (0, null)` is allowed.
...
## Compatibility
In tuple equality, expressions such as `(0, null) == (0, null)` and `(0, null) != (0, null)` are allowed with neither `null` nor the tuple literals having a type.
If someone wrote their own `ValueTuple` types with an implementation of the comparison operator, it would have previously been picked up by overload resolution. But since the new tuple case comes before overload resolution, we would handle this case with tuple comparison instead of relying on the user-defined comparison.
----
Relates to [relational and type testing operators](../../spec/expressions.md#relational-and-type-testing-operators)
Relates to [#190](https://github.com/dotnet/csharplang/issues/190)
Consider the case of a type `struct S` without `operator==`. The comparison `(S?)x == null` is allowed, and it is interpreted as `((S?).x).HasValue`. In tuple equality, the same rule is applied, so `(0, (S?)x) == (0, null)` is also allowed.