Handle T? constraint#27525
Conversation
|
@dotnet/roslyn-compiler please review. #Resolved |
|
@dotnet/roslyn-compiler please review. #Resolved |
|
@cston It looks like there are legitimate test failures #Resolved |
|
@dotnet/roslyn-compiler please review, thanks. |
| _Describe the set of tests that affect flow state._ | ||
|
|
||
| ## `default` | ||
| `default(T)` is `T?` if `T` is a reference type. _Should `default(T?)` be an error?_ |
There was a problem hiding this comment.
reference type [](start = 33, length = 14)
It may be good to explain the meaning of T?, especially for unconstrained T.
Also, the phrasing may need to be adjusted "is a reference or unconstrained type".
Tangent: the null-state of null and the null-state of an instantiation (new) are trivial.
This spec change is unrelated to the PR. It may be better to have a separate PR for the spec change. #Closed
There was a problem hiding this comment.
Added a question about unconstrained type parameters.
In reply to: 195513298 [](ancestors = 195513298)
|
|
||
| internal static ImmutableArray<TypeParameterConstraintClause> MakeTypeParameterConstraints( | ||
| this SourceMethodSymbol containingSymbol, | ||
| internal static ImmutableArray<TypeParameterConstraintClause> MakeTypeParameterConstraintsEarly( |
There was a problem hiding this comment.
MakeTypeParameterConstraintsEarly [](start = 70, length = 33)
Please add comments to clarify the purpose and rules around "early" vs. "late". #Closed
| /// If a type parameter does not have constraints, the corresponding entry in the array is null. | ||
| /// </summary> | ||
| public abstract ImmutableArray<TypeParameterConstraintClause> TypeParameterConstraintClauses { get; } | ||
| public abstract ImmutableArray<TypeParameterConstraintClause> GetTypeParameterConstraintClauses(bool early); |
There was a problem hiding this comment.
GetTypeParameterConstraintClauses [](start = 70, length = 33)
Please comment on early parameter #Closed
| internal ImmutableArray<TypeSymbolWithAnnotations> GetConstraintTypesNoUseSiteDiagnostics(ConsList<TypeParameterSymbol> inProgress, bool early) | ||
| { | ||
| get | ||
| this.EnsureAllConstraintsAreResolved(early: true); |
There was a problem hiding this comment.
EnsureAllConstraintsAreResolved [](start = 17, length = 31)
Is there a difference with just calling EnsureAllConstraintsAreResolved(early)? I would have assumed that calling with early : false would just do a superset of the work that calling with early : true does. #Closed
There was a problem hiding this comment.
The work for early: false is a superset of the early: true, but the early work should be completed for all type parameters before the late work is started for any type parameter. This split (two separate calls), needs to be done somewhere, and it was simpler to do here, in the caller of EnsuresAllConstraintsAreResolved rather than in several implementations of that abstract method. (This should be the only caller of that method that support early: false.)
In reply to: 195540314 [](ancestors = 195540314)
There was a problem hiding this comment.
Thanks. Probably worth a comment #Closed
| return this.GetConstraintTypes(inProgress, early); | ||
| } | ||
|
|
||
| internal ImmutableArray<TypeSymbolWithAnnotations> ConstraintTypesNoUseSiteDiagnostics => GetConstraintTypesNoUseSiteDiagnostics(ConsList<TypeParameterSymbol>.Empty, early: false); |
There was a problem hiding this comment.
=> [](start = 94, length = 3)
Nit: consider breaking the line before => #Closed
| /// order the callers query individual type parameters. | ||
| /// </summary> | ||
| internal abstract void EnsureAllConstraintsAreResolved(); | ||
| internal abstract void EnsureAllConstraintsAreResolved(bool early); |
There was a problem hiding this comment.
Please document early parameter #Closed
| { | ||
| return true; | ||
| } | ||
| return IsReferenceTypeFromConstraintTypes(this.GetConstraintTypesNoUseSiteDiagnostics(inProgress, early: true), inProgress); |
There was a problem hiding this comment.
early [](start = 110, length = 5)
Maybe early should be renamed to minValidation or limitedChecks? #Closed
There was a problem hiding this comment.
Perhaps, although early is the term used to differentiate phases of attribute binding so I re-used the term here.
In reply to: 195542908 [](ancestors = 195542908)
|
@dotnet/roslyn-compiler please review. |
| var constraintTypes = currentBounds.ConstraintTypes; | ||
| if (!ContainingModule.UtilizesNullableReferenceTypes) | ||
| { | ||
| constraintTypes = constraintTypes.SelectAsArray(t => t.SetUnknownNullabilityForReferenceTypes()); |
There was a problem hiding this comment.
SetUnknownNullabilityForReferenceTypes [](start = 75, length = 38)
Can you add a PROTOTYPE marker: I suspect this will need to be revised in the presence of [NonNullTypes] in relevant scope. #Resolved
| new C<A>(); | ||
| } | ||
| }"; | ||
| var comp2 = CreateCompilation(source2, parseOptions: TestOptions.Regular8, references: new[] { comp.EmitToImageReference() }); |
There was a problem hiding this comment.
CreateCompilation [](start = 24, length = 17)
Could you also verify diagnostics when compiling soure+source2? This also applies to other tests below. #Resolved
| { | ||
| static void Main() | ||
| { | ||
| new B<A?>(); |
There was a problem hiding this comment.
B<A?> [](start = 12, length = 5)
I'd be tempted to also test permutations with a type derived from A, such as new B<AChild?>();, and so on. #Resolved
| } | ||
|
|
||
| [Fact] | ||
| public void EmitAttribute_Constraint_Oblivious() |
There was a problem hiding this comment.
EmitAttribute_Constraint_Oblivious [](start = 20, length = 34)
Please add a PROTOTYPE marker to test this with [NonNullTypes]. #Resolved
| } | ||
|
|
||
| [Fact] | ||
| public void ConstraintErrorMultiplePartialDeclarations_01() |
There was a problem hiding this comment.
ConstraintErrorMultiplePartialDeclarations_01 [](start = 20, length = 45)
Maybe test partial declarations with differences in ?. For instance, one with an I constraint, and the other with an I? constraint. #Resolved
There was a problem hiding this comment.
See StaticNullChecking.PartialClassConstraintMismatch.
In reply to: 197928471 [](ancestors = 197928471)
| // (5,9): error CS0454: Circular constraint dependency involving 'T' and 'T' | ||
| // class E<T> where T : A, T? { } | ||
| Diagnostic(ErrorCode.ERR_CircularConstraint, "T").WithArguments("T", "T").WithLocation(5, 9)); | ||
| } |
There was a problem hiding this comment.
} [](start = 8, length = 1)
Is there any interesting scenario with nullability and possible unification on type arguments?
For instance, in where T : I<int>, I<U> we'll complain that U might unify with int.
But in where T : I<int>, I<U?> or where T : I<int?>, I<U?>, I think they cannot unify. #Resolved
There was a problem hiding this comment.
| } | ||
|
|
||
| public static string ToTestDisplayString(this SymbolWithAnnotations symbol) | ||
| public static string ToTestDisplayString(this SymbolWithAnnotations symbol, bool includeNonNullable = false) |
There was a problem hiding this comment.
ToTestDisplayString [](start = 29, length = 19)
Should this new extension method be moved to the "right" place (see comment on method above)? #ByDesign
There was a problem hiding this comment.
It looks like there are more callers of the extension method above than the extension method referenced in the comment. Perhaps the extension method in the comment should be replaced instead.
In reply to: 197936968 [](ancestors = 197936968)
| class E<T> where T : A, T? { }"; | ||
| var comp = CreateCompilation(source, parseOptions: TestOptions.Regular8); | ||
| comp.VerifyDiagnostics( | ||
| // (4,30): error CS0701: 'T?' is not a valid constraint. A type used as a constraint must be an interface, a non-sealed class or a type parameter. |
There was a problem hiding this comment.
error CS0701: 'T?' is not a valid constraint. A type used as a constraint must be an interface, a non-sealed class or a type parameter. [](start = 27, length = 135)
Should this diagnostic message be updated? #ByDesign
| class C<V> where V : V?, V? { }"; | ||
| var comp = CreateCompilation(source, parseOptions: TestOptions.Regular8); | ||
| comp.VerifyDiagnostics( | ||
| // (3,26): error CS0405: Duplicate constraint 'V' for type parameter 'V' |
There was a problem hiding this comment.
V [](start = 63, length = 1)
Should we say "Duplicate constraint 'V?'..." instead? #Pending
There was a problem hiding this comment.
Yes, top-level nullability should be included in the message. This requires allowing TypeSymbolWithAnnotations as a Diagnostic argument. I have that change in the next PR.
In reply to: 197943396 [](ancestors = 197943396)
| // (2,9): error CS0454: Circular constraint dependency involving 'U' and 'U' | ||
| // class B<U> where U : U?, U { } | ||
| Diagnostic(ErrorCode.ERR_CircularConstraint, "U").WithArguments("U", "U").WithLocation(2, 9)); | ||
| } |
There was a problem hiding this comment.
} [](start = 8, length = 1)
I don't remember the LDM decision on whether top-level nullability constraints need to agree. For instance, where T : I?, U where U : class. #Resolved
There was a problem hiding this comment.
We're not checking nullability of constraints currently, but according to the notes from 4/25, nullability of constraints should agree. nullable-reference-types.md contains a reference to those notes for now.
In reply to: 197943706 [](ancestors = 197943706)
| // (8,40): error CS0701: 'T?' is not a valid constraint. A type used as a constraint must be an interface, a non-sealed class or a type parameter. | ||
| // void F3<T>() where T : struct, T? { } | ||
| Diagnostic(ErrorCode.ERR_BadBoundType, "T?").WithArguments("T?").WithLocation(8, 40), | ||
| // (9,32): error CS0246: The type or namespace name 'A' could not be found (are you missing a using directive or an assembly reference?) |
There was a problem hiding this comment.
The type or namespace name 'A' [](start = 41, length = 30)
Is the error on missing type A intentional? #Resolved
There was a problem hiding this comment.
| void F3<T>() where T : struct, T? { } | ||
| void F4<T>() where T : A, T? { } | ||
| void F5<T, U>() where U : T? { } | ||
| void F6<T, U>() where T : class where U : T? { } |
There was a problem hiding this comment.
F6 [](start = 13, length = 2)
It would be good to test the instantiations on F6 and F7 if they aren't already covered elsewhere.
Same in some tests below (like NullableTInConstraint_06).
Update: I think you're intentionally keeping that for a follow-up PR, which makes sense. #Resolved
| ### Null tests | ||
| _Describe the set of tests that affect flow state._ | ||
|
|
||
| ## `default` |
There was a problem hiding this comment.
Please update the feature doc to explain T? constraints. #Resolved
| _syntax.ConstraintClauses, | ||
| _syntax.Identifier.GetLocation(), | ||
| diagnostics); | ||
| lock (_declarationDiagnostics) |
There was a problem hiding this comment.
Consider another check to _lazyTypeParameterConstraints.IsDefault, as MakeTypeParameterConstraintsEarly is not a small function.
There was a problem hiding this comment.
If _lazyTypeParameterConstraints.IsDefault fails, another thread has already updated the field so there shouldn't be any lock contention from with that thread.
Will leave as is unless there are other concerns.
In reply to: 198667734 [](ancestors = 198667734)
| { | ||
| // Late step. | ||
| var diagnostics = DiagnosticBag.GetInstance(); | ||
| var constraints = this.MakeTypeParameterConstraintsLate(TypeParameters, clauses, diagnostics); |
There was a problem hiding this comment.
Same suggestion on the check to _lazyTypeParameterConstraints.IsEarly() to avoid unnecessary lock contention.
|
|
||
| var clauses = _lazyTypeParameterConstraints; | ||
| return (clauses.Length > 0) ? clauses[ordinal] : null; | ||
| return (clauses.Length > 0) ? clauses[ordinal] : TypeParameterConstraintClause.Empty; |
There was a problem hiding this comment.
We should probably add a comment to _lazyTypeParameterConstraints that multiple accesses like this are only safe if you copy it to a local variable first, as it's now a multi-stage initialization that could change between the first and second accesses. Same goes for the rest of the multi-stage initialization changes in this PR. #Resolved
333fred
left a comment
There was a problem hiding this comment.
LGTM (commit 21), with some suggestions for followup. I am slightly concerned that future changes might introduce subtle threading issues with the double initialization strategy we're using, so I'd like us to at least comment about accessing those double-initialized fields now.
Binding of constraints is now split into two steps: early and late.
In the early step, constraint types are bound but with no additional checks. In the late step, constraint types are checked for validity and duplicates, and type parameter "bounds" (
EffectiveBaseandEffectiveInterface) are calculated.Most
TypeParameterproperties simply ensure both steps have completed (as before), butIsValueTypeandIsReferenceTypenow only ensure the early step has completed.Fixes #27289.