13.8.3 The switch statement - edit loses base type text
In revising the paragraph defining governing type:
The governing type of a
switchstatement … Otherwise, exactly one user-defined implicit conversion operator (§13.4) shall exist from the type of the switch expression or a base type of this type to one of the following possible governing types …
into a bulleted list which handles the inclusion of nullable value types the Proposal has lost the base type text above:
… or a base type of this type …
producing the bullet:
• Otherwise, exactly one user-defined implicit conversion shall exist from the type of the switch expression to one of the following possible governing types …
The reason for this deletion is not immediately apparent, and it is quite possible this is a regression.
Proposal:
Assuming this is a regression, add the lost phrase into the Proposal's bulleted item.
We think we should probably reject both changes in that bullet, leaving:
Otherwise, exactly one user-defined implicit conversion operator (§12.5) shall exist from the type of the switch expression or a base type of this type to one of the following possible governing types: sbyte, byte, short, ushort, int, uint, long, ulong, char, string, or, a nullable value type corresponding to one of those types.
// NOTE: For (2) we use an approach similar to native compiler's approach, but call into the common code for analyzing user defined implicit conversions.
// NOTE: (a) Compute the set of types D from which user-defined conversion operators should be considered by considering only the source type.
// NOTE: (b) Instead of computing applicable user defined implicit conversions U from the source type to a specific target type,
// NOTE: we compute these from the source type to ANY target type.
// NOTE: (c) From the conversions in U, select the most specific of them that targets a valid switch governing type
// SPEC VIOLATION: Because we use the same strategy for computing the most specific conversion, as the Dev10 compiler did (in fact
// SPEC VIOLATION: we share the code), we inherit any spec deviances in that analysis. Specifically, the analysis only considers
// SPEC VIOLATION: which conversion has the least amount of lifting, where a conversion may be considered to be in unlifted form,
// SPEC VIOLATION: half-lifted form (only the argument type or return type is lifted) or fully lifted form. The most specific computation
// SPEC VIOLATION: looks for a unique conversion that is least lifted. The spec, on the other hand, requires that the conversion
// SPEC VIOLATION: be *unique*, not merely most use the least amount of lifting among the applicable conversions.
// SPEC VIOLATION: This introduces a SPEC VIOLATION for the following tests in the native compiler:
// NOTE: // See test SwitchTests.CS0166_AggregateTypeWithMultipleImplicitConversions_07
// NOTE: struct Conv
// NOTE: {
// NOTE: public static implicit operator int (Conv C) { return 1; }
// NOTE: public static implicit operator int (Conv? C2) { return 0; }
// NOTE: public static int Main()
// NOTE: {
// NOTE: Conv? D = new Conv();
// NOTE: switch(D)
// NOTE: { ...
// SPEC VIOLATION: Native compiler allows the above code to compile
// SPEC VIOLATION: even though there are two user-defined implicit conversions:
// SPEC VIOLATION: 1) To int type (applicable in normal form): public static implicit operator int (Conv? C2)
// SPEC VIOLATION: 2) To int? type (applicable in lifted form): public static implicit operator int (Conv C)
// NOTE: // See also test SwitchTests.TODO
// NOTE: struct Conv
// NOTE: {
// NOTE: public static implicit operator int? (Conv C) { return 1; }
// NOTE: public static implicit operator string (Conv? C2) { return 0; }
// NOTE: public static int Main()
// NOTE: {
// NOTE: Conv? D = new Conv();
// NOTE: switch(D)
// NOTE: { ...
// SPEC VIOLATION: Native compiler allows the above code to compile too
// SPEC VIOLATION: even though there are two user-defined implicit conversions:
// SPEC VIOLATION: 1) To string type (applicable in normal form): public static implicit operator string (Conv? C2)
// SPEC VIOLATION: 2) To int? type (applicable in half-lifted form): public static implicit operator int? (Conv C)
// SPEC VIOLATION: This occurs because the native compiler compares the applicable conversions to find one with the least amount
// SPEC VIOLATION: of lifting, ignoring whether the return types are the same or not.
// SPEC VIOLATION: We do the same to maintain compatibility with the native compiler.
// (a) Compute the set of types D from which user-defined conversion operators should be considered by considering only the source type.
var d = ArrayBuilder<NamedTypeSymbol>.GetInstance();
ComputeUserDefinedImplicitConversionTypeSet(source, t: null, d: d, useSiteDiagnostics: ref useSiteDiagnostics);
// (b) Instead of computing applicable user defined implicit conversions U from the source type to a specific target type,
// we compute these from the source type to ANY target type. We will filter out those that are valid switch governing
// types later.
var ubuild = ArrayBuilder<UserDefinedConversionAnalysis>.GetInstance();
ComputeApplicableUserDefinedImplicitConversionSet(null, source, target: null, d: d, u: ubuild, useSiteDiagnostics: ref useSiteDiagnostics, allowAnyTarget: true);
d.Free();
ImmutableArray<UserDefinedConversionAnalysis> u = ubuild.ToImmutableAndFree();
// (c) Find that conversion with the least amount of lifting
int? best = MostSpecificConversionOperator(conv => conv.ToType.IsValidSwitchGoverningType(isTargetTypeOfUserDefinedOp: true), u);
if (best != null)
{
return UserDefinedConversionResult.Valid(u, best.Value);
}
Neal has taken the job of formalizing what the compiler actually does as specese.
struct S
{
public static implicit operator int?(S x)
{
return 0;
}
public static implicit operator long(S x)
{
return 0;
}
static void Main()
{
S? x = null;
switch (x) // No error
{
}
}
}
The reason for deleting the possibility of conversion from a base type is that is already considered a conversion from the type itself. So that part of the change in the WD is fine, I think.
The rest of the changes can be deferred to a later spec revision.
All comments prior to this one were before we removed section 7. I've updated the title only.
I've labeled this as a bug for now, but it might be clarity really - I haven't delved into the details of what actually still needs changing here. (And it may be subsumed by other switch changes anyway.)
Assigning to @gafter to investigate before the next meeting.
C# 7 introduces pattern-matching, by which any type can be used as an input to a switch statement. If the previous language version would have converted the switch expression to some type, then it is so converted (otherwise it isn't). Rather than trying to patch up the C# 6 version of these rules and then rewriting them again for C# 7, I suggest we make the correction when writing up the changes for pattern-matching.
I will figure out what we need to do for this.
@gafter: Ping for this one too? (I'm trying to take stock of what we really need to do for C# 7...)
Meeting 2023-09-06: Defer to C# 8.