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

Enhancing the Common Type Specification #2823

Open
gafter opened this issue Sep 24, 2019 · 0 comments
Open

Enhancing the Common Type Specification #2823

gafter opened this issue Sep 24, 2019 · 0 comments
Assignees
Labels

Comments

@gafter
Copy link
Member

gafter commented Sep 24, 2019

Enhancing the Common type spec

The common type (C# 5 ECMA spec section 12.6.3.1.5) says

12.6.3.15 Finding the best common type of a set of expressions

In some cases, a common type needs to be inferred for a set of expressions. In particular, the element types of implicitly typed arrays and the return types of anonymous functions with block bodies are found in this way.
The best common type for a set of expressions E1…Em is determined as follows:

  • A new unfixed type variable X is introduced.
  • For each expression Ei an output type inference (§12.6.3.7) is performed from it to X.
  • X is fixed (§12.6.3.12), if possible, and the resulting type is the best common type.
  • Otherwise inference fails.

[Note: Intuitively this inference is equivalent to calling a method

void M<X>(X x1 … X xm)

with the Ei as arguments and inferring X. end note]

First, we note that the code would be an error if any of the expressions Ei cannot be converted to the type to which X was fixed. In order to recognize this as a form of failure, we modify the penultimate bullet to be

  • X is fixed (§12.6.3.12), if possible. If every Ei can be implicitly converted to the resulting type then it is the best common type.

This change has no effect on the legality or meaning of any C# 8 program, but it simplifies the generalization to target typing and the discussion below.

This best common type algorithm is used for three things in the specification:

  • implicitly typed array creation (new [] { e1, e2 }),
  • finding the return type of a lambda ((b, e1, e2) => {if (b) return e1; else return e2; }), and
  • finding the implicit type of a switch expression (b switch { true => e1, false => e2 }).

It is not used for the conditional expression b?e1:e2, or the null coalescing operator e1??e2 which each has has its own specification for the common type that is based on the existence of a conversion from expression rather than a conversion from type.

These distinct common type specifications can produce different results. For example, there is no best common type for a value of an enumeration type and the integer constant zero, but the specification for the conditional operator selects the enumeration type as the result. Given that we have these three different specifications for best common type (for different constructs) that produce different results, I propose that we not attempt to unify them.

Proposed changes

There are five pending proposals to make changes to these common type algorithms:

There was some concern expressed in the LDM that once we enable target typing for a construct (whether as a fallback or always available), adding new enhancements to the common type algorithm would be a breaking change because it would give the expression a type, which could affect overload resolution. For this reason we fear that if we add target typing to a construct this might be our last opportunity to add any further enhancements to the common type algorithm.

Moreover, the fact that we have target typing for the switch-expression may mean that our window to enhance the common type algorithm has already closed. Here is an illustration of the kind of break that would concern us:

class C
{
    static void Main()
    {
        int i = 1;
        M(1 switch { 1 => 1, 2 => null });
    }

    static void M(int ? x) { }
    static void M(short ? y) { }
}

class X
{
    public static implicit operator X(int? v) => null;
}

class Y
{
    public static implicit operator Y(short? v) => null;
}

In C# 8, we select the second overload (it is a better conversion from expression because the conversion to short? is a better conversion than the conversion to int?). However, if we extend the common type algorithm to assign the type int? to the switch expression, we would select the first overload (it is a better conversion from expression because int? is the type of the expression).

Let us survey the set of user requests for enhancements:

  1. Common types involving nullable, such as Permit ternary operation with int? and double operands #881 and Champion "Nullable-enhanced common type" #33
  2. The common type of two class types being their most derived common base type.
  3. The common type of two types being their common interface type. If more than one, possibly an intersection type (a concept that does not yet exist).
  4. The common type of two unrelated types being a union type (a concept that does not yet exist). This is also needed to produce a common type for two distinct instances of a variant interface.
  5. The common type of two members of a discriminated union being the common DU type. This is related to (2) but for situations that do not arise today. Because these will be new scenarios, we are not concerned about breaking changes.

Target-typing only as a fallback?

There was some concern expressed in the LDM that it would be nice to align the handling of the conditional and null coalescing expressions (where target typing is proposed only as a fallback) with the handling of the switch expression (where target typing is available even if there is a common type). However, it is a breaking change to add target typing to an existing construct that has a type. Here is a demonstration:

class C
{
    static void Main()
    {
        bool c = true;
        M(c ? 1 : 2); // first M in C# 8, ambiguous if ?: is target-typed
    }

    static void M(X x) { }
    static void M(Y y) { }
}

class X
{
    public static implicit operator X(int v) => null;
}

class Y
{
    public static implicit operator Y(short v) => null;
}

The incompatibility arises when there is a language-defined conversion from expression for an expression that has a type and that depends on some aspect of the expression aside from its type (e.g. its value). It arises due to

  • implicit constant expression conversions (as illustrated above),
  • implicit enumeration conversions,
  • implicit interpolated string conversions, and
  • switch expression conversions where the contained expressions all qualify based on this list.

If we elect to permit target-typing of conditional expressions and null coalescing expressions (when they have a common type), those would also be added to this list.

We can avoid this breaking change by adding support for target-typing of conditional expressions and null coalescing expressions only when they have no common type.

Recommendation

I believe that the proposed enhancements to the three common type algorithms are less compelling once the relevant constructs are target-typed. I believe we should add target-typing as a fallback to the conditional expression and the null coalescing expression to minimize the break to existing code. The other enhancements to the common type algorithms invent result types not appearing among the inputs (e.g. long? for input types int? and long), which violates one of the principle design invariants for type inference (only produce as a result a type which was one of the inputs), and which can also change the meaning of existing code.

The disadvantage of taking target-typing as a fallback only is that code such as this will not compile:

short s = b ? 1 : 2;

This is something we have long lived with, and (perhaps surprisingly) it not among the use cases called out by customers in public discussions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants