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

Specify generic constraints added to support nullable reference types in C# 8 #1178

Open
wants to merge 6 commits into
base: draft-v8
Choose a base branch
from

Conversation

BillWagner
Copy link
Member

Add the class? and notnull constraint.

The default constraint is added in C# 9, so is not included.

Bring in the normative text from dotnet#700.

Some text is removed because of the decision on normative language in our September meeting.
@BillWagner BillWagner marked this pull request as ready for review September 18, 2024 20:44
@BillWagner BillWagner added the meeting: discuss This issue should be discussed at the next TC49-TG2 meeting label Sep 18, 2024
@BillWagner
Copy link
Member Author

@jskeet I added the meeting discuss label on this. I doubt that it's final, but it's worth a first look.

| 'unmanaged'
;
secondary_constraints
: interface_type
: interface_type '?'?
| type_parameter
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't secondary_constraints allow type_parameter ? as the first secondary constraint? Seems strange to only allow the ? in subsequent secondary constraints. E.g.

public class C<T, U, V>
    where T : U?, V
{
}

vs.

public class C<T, U, V>
    where T : U, V?
{
}

standard/classes.md Show resolved Hide resolved
@@ -532,7 +532,7 @@ type_argument
;
```
Each type argument shall satisfy any constraints on the corresponding type parameter ([§15.2.5](classes.md#1525-type-parameter-constraints)).
Each type argument shall satisfy any constraints on the corresponding type parameter ([§15.2.5](classes.md#1525-type-parameter-constraints)). A type argument whose nullability doesn't match the nullability of the type parameter satisfies the constraint with the exception that a nullable value type does not satisfy the struct constraint. A warning may be issued when the nullability of a type argument does not satisfy the nullability requirements of the constraint.
Copy link
Contributor

Choose a reason for hiding this comment

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

Does not satisfy the value type constraint. struct is the keyword in the constraint syntax but it's not the name of the constraint.

```
The *non_nullable_non_value_type_parameter* in *nullable_type_parameter* shall be a type parameter that isn’t constrained to be a value type.
Copy link
Contributor

Choose a reason for hiding this comment

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

"shall" can give the wrong idea that the compiler would report an error otherwise. Could clarify with a note saying that the same syntax can be used with a type parameter that is constrained to be a value type, but it means Nullable<T> in that case.

Copy link
Member Author

Choose a reason for hiding this comment

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

Also waiting for more eyes here. I see your point, but I don't have the right suggestion yet.

```
The *non_nullable_non_value_type_parameter* in *nullable_type_parameter* shall be a type parameter that isn’t constrained to be a value type.

In *nullable_type_parameter*, the annotation `?` indicates the intent that type arguments of this type are nullable. The absence of the annotation `?` indicates the intent that type arguments of this type are non-nullable.
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 that the intention is for the type argument to be nullable.

class C {
    void M<T>()
        where T : class
    {
        T?[] a = {};
    }
    
    void N()
    {
        M<object>();
    }
}

In the declaration T?[] a = {};, T? is a nullable_type_parameter. The corresponding type argument is object in M<object>();, but that one isn't nullable.

Copy link
Contributor

@KalleOlaviNiemitalo KalleOlaviNiemitalo Sep 19, 2024

Choose a reason for hiding this comment

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

On second thought, I'm not sure T? is a nullable_type_parameter after all. It matches the nullable_type_parameter grammar but nullable_type_parameter doesn't seem to be referenced by any other grammar rule, so it is not applicable in this context (nor in any other).

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm waiting for others to weigh in here as well. I see your point, but I don't have an obvious suggestion yet.

@jskeet
Copy link
Contributor

jskeet commented Sep 19, 2024

(I'll wait for responses to @KalleOlaviNiemitalo's questions before reviewing, if that's okay.)

Copy link
Contributor

@Nigel-Ecma Nigel-Ecma left a comment

Choose a reason for hiding this comment

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

Some comments plus ANTLRizing some grammar rules.

I haven’t considered per se what should (not) be in the Standard in regards to the specification of NRT.

Comment on lines +423 to +426
: interface_type '?'?
| type_parameter '?'?
| secondary_constraints ',' interface_type '?'?
| secondary_constraints ',' type_parameter '?'?
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should review the grammar here. I can't add this as a suggested changed as it covers non-altered lines, so just code. The rules here pre-date the use of ANTLR, if we ANTLRize them and remove unneeded left-recursion (as we do elsewhere) we get:

type_parameter_constraints
    : primary_constraint (',' secondary_constraints)? (',' constructor_constraint)?
    | secondary_constraints (',' constructor_constraint)?
    | constructor_constraint
    ;

primary_constraint
    : class_type '?'?
    | 'class' '?'?
    | 'struct'
    | 'notnull'
    | 'unmanaged'
    ;

secondary_constraint
    : interface_type '?'?
    | type_parameter '?'?
    ;

secondary_constraints
    : secondary_constraint (',' secondary_constraint)*
    ;

Optionally to improve readability we could replace the '?'? by adding a rule:

nullable_type_attribute
    : '?'
    ;

type_parameter_constraints
    : primary_constraint (',' secondary_constraints)? (',' constructor_constraint)?
    | secondary_constraints (',' constructor_constraint)?
    | constructor_constraint
    ;

primary_constraint
    : class_type nullable_type_attribute?
    | 'class' nullable_type_attribute?
    | 'struct'
    | 'notnull'
    | 'unmanaged'
    ;

secondary_constraint
    : interface_type nullable_type_attribute?
    | type_parameter nullable_type_attribute?
    ;

secondary_constraints
    : secondary_constraint (',' secondary_constraint)*
    ;

The introduced * nullable_type_attribute* would then also need to be used in other grammar rules where the literal '?' is used as a type attribute and not in places where it is not, e.g. for the null conditional operations.

Copy link
Member Author

@BillWagner BillWagner Sep 23, 2024

Choose a reason for hiding this comment

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

I like the second one. When I was working through the grammar, I found '?'? hard to mentally parse and read through the grammar.

Copy link
Contributor

Choose a reason for hiding this comment

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

+1 for a rule for nullable_type_attribute.


A secondary constraint can be a *type_parameter* or *interface_type*.
A secondary constraint can be a *type_parameter* or *interface_type*, either optionally followed by `?`.
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn’t read well to me in this context, given the comment on 426 above I’ll offer two alternatives:

Suggested change
A secondary constraint can be a *type_parameter* or *interface_type*, either optionally followed by `?`.
A secondary constraint can be a *type_parameter* or *interface_type*, either optionally nullable.

Or if the line 426 added rule is adopted:

Suggested change
A secondary constraint can be a *type_parameter* or *interface_type*, either optionally followed by `?`.
A secondary constraint can be a *type_parameter* or *interface_type*, optionally followed by a *nullable_type_attribute*.

Comment on lines +612 to +614
A type parameter is *known to be a non-nullable reference type* if it has the non-nullable reference type constraint or its effective base class is not `object` or `System.ValueType`.
A type parameter is *known to be a reference type* if it has the non-nullable reference type constraint, reference type constraint or its effective base class is not `object` or `System.ValueType`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Could this be simplified to:

Suggested change
A type parameter is *known to be a non-nullable reference type* if it has the non-nullable reference type constraint or its effective base class is not `object` or `System.ValueType`.
A type parameter is *known to be a reference type* if it has the non-nullable reference type constraint, reference type constraint or its effective base class is not `object` or `System.ValueType`.
A type parameter is *known to be a non-nullable reference type* if it has the non-nullable reference type constraint or its effective base class is not `object` or `System.ValueType`.
A type parameter is *known to be a reference type* if it has the reference type constraint or its known to be a non-nullable reference type.

Copy link
Contributor

Choose a reason for hiding this comment

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

its => it's in the suggestion (line 614) - but then I like it.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think we're not to use contractions, so "it is".

@@ -878,7 +885,7 @@ All members of a generic class can use type parameters from any enclosing class,
> class C<V>
> {
> public V f1;
> public C<V> f2 = null;
> public C<V> f2 = null!;
Copy link
Contributor

Choose a reason for hiding this comment

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

It is unclear to me why in the original f2 was initialised, and f1 not, when both are set in the constructor. Now a suppression has been added…

I suggest a comment explaining the suppression, or the use of default (which leaves the explanation for the section on default to handle), or just drop the initialisation.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would be better to drop the initialization.

@@ -532,7 +532,7 @@ type_argument
;
```

Each type argument shall satisfy any constraints on the corresponding type parameter ([§15.2.5](classes.md#1525-type-parameter-constraints)).
Each type argument shall satisfy any constraints on the corresponding type parameter ([§15.2.5](classes.md#1525-type-parameter-constraints)). A type argument whose nullability doesn't match the nullability of the type parameter satisfies the constraint with the exception that a nullable value type does not satisfy the value type constraint. A warning may be issued when the nullability of a type argument does not satisfy the nullability requirements of the constraint.
Copy link
Contributor

Choose a reason for hiding this comment

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

I don’t think this needs to be phrased in terms of value type exceptions, the point being that reference type nullability is a “soft” constraint. Maybe:

Suggested change
Each type argument shall satisfy any constraints on the corresponding type parameter ([§15.2.5](classes.md#1525-type-parameter-constraints)). A type argument whose nullability doesn't match the nullability of the type parameter satisfies the constraint with the exception that a nullable value type does not satisfy the value type constraint. A warning may be issued when the nullability of a type argument does not satisfy the nullability requirements of the constraint.
Each type argument shall satisfy any constraints on the corresponding type parameter ([§15.2.5](classes.md#1525-type-parameter-constraints)). A reference type argument whose nullability doesnt match the nullability of the type parameter satisfies the constraint; however a warning may be issued.

I see that @KalleOlaviNiemitalo has commented on the use of “shall” here, I'll defer that to @RexJaeschke.

Comment on lines 604 to 611

nullable_type_parameter
: non_nullable_non_value_type_parameter '?'
;

non_nullable_non_value_type_parameter
: type_parameter
;
Copy link
Contributor

Choose a reason for hiding this comment

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

This grammar is wrong/pointless, as @KalleOlaviNiemitalo has commented the added rules are not used anywhere.

Unfortunately I’m not sure what the intended semantics are here.

Guessing: is the addition of the nullable type attribute meant to require the type argument to be a nullable reference and/or value type, i.e. it’s an additional kind of primary_constraint or secondary_constraint?

Copy link
Member Author

Choose a reason for hiding this comment

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

That was my mistake. I added the additional rules in the latest commit.

Note that the grammar additions for non_nullable_reference_type and nullable_reference_type are in #1089

Copy link
Contributor

@jskeet jskeet left a comment

Choose a reason for hiding this comment

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

I like Nigel's suggestions in general. My guess is that the currently-unused rules are intended to be used in the future? Would be good to know.

Comment on lines +423 to +426
: interface_type '?'?
| type_parameter '?'?
| secondary_constraints ',' interface_type '?'?
| secondary_constraints ',' type_parameter '?'?
Copy link
Contributor

Choose a reason for hiding this comment

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

+1 for a rule for nullable_type_attribute.

Comment on lines +612 to +614
A type parameter is *known to be a non-nullable reference type* if it has the non-nullable reference type constraint or its effective base class is not `object` or `System.ValueType`.
A type parameter is *known to be a reference type* if it has the non-nullable reference type constraint, reference type constraint or its effective base class is not `object` or `System.ValueType`.
Copy link
Contributor

Choose a reason for hiding this comment

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

its => it's in the suggestion (line 614) - but then I like it.

@@ -878,7 +885,7 @@ All members of a generic class can use type parameters from any enclosing class,
> class C<V>
> {
> public V f1;
> public C<V> f2 = null;
> public C<V> f2 = null!;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would be better to drop the initialization.

@BillWagner
Copy link
Member Author

@jskeet

My guess is that the currently-unused rules are intended to be used in the future? Would be good to know.

Which rules do you think are unused? I didn't see any.

@jskeet
Copy link
Contributor

jskeet commented Sep 25, 2024

@jskeet

My guess is that the currently-unused rules are intended to be used in the future? Would be good to know.

Which rules do you think are unused? I didn't see any.

non_nullable_non_value_type_parameter and nullable_type_parameter - see Nigel's comments.

@BillWagner
Copy link
Member Author

@jskeet

My guess is that the currently-unused rules are intended to be used in the future? Would be good to know.

Which rules do you think are unused? I didn't see any.

non_nullable_non_value_type_parameter and nullable_type_parameter - see Nigel's comments.

Got it. I'd missed some edits. See latest commit.

standard/types.md Outdated Show resolved Hide resolved
Co-authored-by: Nigel-Ecma <perryresearch@zoot.net.nz>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
meeting: discuss This issue should be discussed at the next TC49-TG2 meeting
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants