Conditional expressions (#911)

This proposal introduces a conditional operator of the form:

```
if cond then value1 else value2
```

Co-authored-by: josh11b <josh11b@users.noreply.github.com>
Co-authored-by: Chandler Carruth <chandlerc@gmail.com>
This commit is contained in:
Richard Smith
2022-01-28 20:07:45 -08:00
committed by GitHub
co-authored by josh11b Chandler Carruth
parent 80f402e2b6
commit f6cbd2231e
4 changed files with 850 additions and 0 deletions
+13
View File
@@ -13,6 +13,7 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
- [Overview](#overview)
- [Operators](#operators)
- [Conversions and casts](#conversions-and-casts)
- [`if` expressions](#if-expressions)
<!-- tocstop -->
@@ -57,3 +58,15 @@ fn Baz(n: i64) {
Bar(n);
}
```
## `if` expressions
An [`if` expression](if.md) chooses between two expressions.
```
fn Run(args: Span(StringView)) {
var file: StringView = if args.size() > 1 then args[1] else "/dev/stdin";
}
```
`if` expressions are analogous to `?:` ternary expressions in C and C++.
+243
View File
@@ -0,0 +1,243 @@
# `if` expressions
<!--
Part of the Carbon Language project, under the Apache License v2.0 with LLVM
Exceptions. See /LICENSE for license information.
SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
-->
<!-- toc -->
## Table of contents
- [Overview](#overview)
- [Syntax](#syntax)
- [Semantics](#semantics)
- [Finding a common type](#finding-a-common-type)
- [Symmetry](#symmetry)
- [Same type](#same-type)
- [Implicit conversions](#implicit-conversions)
- [Alternatives considered](#alternatives-considered)
- [References](#references)
<!-- tocstop -->
## Overview
An `if` expression is an expression of the form:
> `if` _condition_ `then` _value1_ `else` _value2_
The _condition_ is converted to a `bool` value in the same way as the condition
of an `if` statement.
> **Note:** These conversions have not yet been decided.
The _value1_ and _value2_ are implicitly converted to their
[common type](#finding-a-common-type), which is the type of the `if` expression.
## Syntax
`if` expressions have very low precedence, and cannot appear as the operand of
any operator, except as the right-hand operand in an assignment. They can appear
in other context where an expression is permitted, such as within parentheses,
as the operand of a `return` statement, as an initializer, or in a
comma-separated list such as a function call.
The _value1_ and _value2_ expressions are arbitrary expressions, and can
themselves be `if` expressions. _value2_ extends as far to the right as
possible. An `if` expression can be parenthesized if the intent is for _value2_
to end earlier.
```
// OK, same as `if cond then (1 + 1) else (2 + (4 * 6))`
var a: i32 = if cond then 1 + 1 else 2 + 4 * 6;
// OK
var b: i32 = (if cond then 1 + 1 else 2) + 4 * 6;
```
An `if` keyword at the start of a statement is always interpreted as an
[`if` statement](/docs/design/control_flow/conditionals.md), never as an `if`
expression, even if it is followed eventually by a `then` keyword.
## Semantics
The converted _condition_ is evaluated. If it evaluates to `true`, then the
converted _value1_ is evaluated and its value is the result of the expression.
Otherwise, the converted _value2_ is evaluated and its value is the result of
the expression.
## Finding a common type
The common type of two types `T` and `U` is `(T as CommonType(U)).Result`, where
`CommonType` is the `Carbon.CommonType` constraint. `CommonType` is notionally
defined as follows:
```
constraint CommonType(U:! CommonTypeWith(Self)) {
extend CommonTypeWith(U) where .Result == U.Result;
}
```
The actual definition is a bit more complex than this, as described in
[symmetry](#symmetry).
The interface `CommonTypeWith` is used to customize the behavior of
`CommonType`:
```
interface CommonTypeWith(U:! Type) {
let Result:! Type
where Self is ImplicitAs(.Self) and
U is ImplicitAs(.Self);
}
```
The implementation `A as CommonTypeWith(B)` specifies the type that `A` would
like to result from unifying `A` and `B` as its `Result`.
_Note:_ It is required that both types implicitly convert to the common type.
Some blanket `impl`s for `CommonTypeWith` are provided as part of the prelude.
These are described in the following sections.
_Note:_ The same mechanism is expected to eventually be used to compute common
types in other circumstances.
### Symmetry
The common type of `T` and `U` should always be the same as the common type of
`U` and `T`. This is enforced in two steps:
- A `SymmetricCommonTypeWith` interface implicitly provides a
`B as CommonTypeWith(A)` implementation whenever one doesn't exist but an
`A as CommonTypeWith(B)` implementation exists.
- `CommonType` is defined in terms of `SymmetricCommonTypeWith`, and requires
that both `A as SymmetricCommonTypeWith(B)` and
`B as SymmetricCommonTypeWith(A)` produce the same type.
The interface `SymmetricCommonTypeWith` is an implementation detail of the
`CommonType` constraint. It is defined and implemented as follows:
```
interface SymmetricCommonTypeWith(U:! Type) {
let Result:! Type
where Self is ImplicitAs(.Self) and
U is ImplicitAs(.Self);
}
match_first {
impl [T:! Type, U:! CommonTypeWith(T)] T as SymmetricCommonTypeWith(U) {
let Result:! Type = U.Result;
}
impl [U:! Type, T:! CommonTypeWith(U)] T as SymmetricCommonTypeWith(U) {
let Result:! Type = T.Result;
}
}
```
The `SymmetricCommonTypeWith` interface is not exported, so user-defined `impl`s
can't be defined, and only the two blanket `impl`s above are used. The
`CommonType` constraint is then defined as follows:
```
constraint CommonType(U:! SymmetricCommonTypeWith(Self)) {
extend SymmetricCommonTypeWith(U) where .Result == U.Result;
}
```
When computing the common type of `T` and `U`, if only one of the types provides
a `CommonTypeWith` implementation, that determines the common type. If both
types provide a `CommonTypeWith` implementation and their `Result` types are the
same, that determines the common type. Otherwise, if both types provide
implementations but their `Result` types differ, there is no common type, and
the `CommonType` constraint is not met. For example, given:
```
// Implementation #1
impl [T:! Type] MyX as CommonTypeWith(T) {
let Result:! Type = MyX;
}
// Implementation #2
impl [T:! Type] MyY as CommonTypeWith(T) {
let Result:! Type = MyY;
}
```
`MyX as CommonTypeWith(MyY)` will select #1, and `MyY as CommonTypeWith(MyX)`
will select #2, but the constraints on `MyX as CommonType(MyY)` will not be met
because result types differ.
### Same type
If `T` is the same type as `U`, the result is that type:
```
final impl [T:! Type] T as CommonTypeWith(T) {
let Result:! Type = T;
}
```
_Note:_ This rule is intended to be considered more specialized than the other
rules in this document.
Because this `impl` is declared `final`, `T.(CommonType(T)).Result` is always
assumed to be `T`, even in contexts where `T` involves a generic parameter and
so the result would normally be an unknown type whose type-of-type is `Type`.
```
fn F[T:! Hashable](c: bool, x: T, y: T) -> HashCode {
// OK, type of `if` expression is `T`.
return (if c then x else y).Hash();
}
```
### Implicit conversions
If `T` implicitly converts to `U`, the common type is `U`:
```
impl [T:! Type, U:! ImplicitAs(T)] T as CommonTypeWith(U) {
let Result:! Type = T;
}
```
_Note:_ If an implicit conversion is possible in both directions, and no more
specific implementation exists, the constraints on `T as CommonType(U)` will not
be met because `(T as CommonTypeWith(U)).Result` and
`(U as CommonTypeWith(T)).Result` will differ. In order to define a common type
for such a case, `CommonTypeWith` implementations in both directions must be
provided to override the blanket `impl`s in both directions:
```
impl MyString as CommonTypeWith(YourString) {
let Result:! Type = MyString;
}
impl YourString as CommonTypeWith(MyString) {
let Result:! Type = MyString;
}
var my_string: MyString;
var your_string: YourString;
// The type of `also_my_string` is `MyString`.
var also_my_string: auto = if cond then my_string else your_string;
```
## Alternatives considered
- [Provide no conditional expression](/proposals/p0911.md#no-conditional-expression)
- Use
[`cond ? expr1 : expr2`, like in C and C++](/proposals/p0911.md#use-c-syntax)
syntax
- Use [`if (cond) expr1 else expr2`](/proposals/p0911.md#no-then) syntax
- Use
[`if (cond) then expr1 else expr2`](/proposals/p0911.md#require-parentheses-around-the-condition)
syntax
- Allow
[`1 + if cond then expr1 else expr2`](/proposals/p0911.md#never-require-enclosing-parentheses)
- [Only require one `impl` to specify the common type if implicit conversions in both directions are possible](/proposals/p0911.md#implicit-conversions-in-both-directions)
- [Introduce special rules for lvalue conditionals](/proposals/p0911.md#support-lvalue-conditionals)
## References
- Proposal
[#911: Conditional expressions](https://github.com/carbon-language/carbon-lang/pull/911).
+1
View File
@@ -73,6 +73,7 @@ The following words are interpreted as keywords:
- `protected`
- `return`
- `returned`
- `then`
- `var`
- `virtual`
- `where`
+593
View File
@@ -0,0 +1,593 @@
# Conditional expressions
<!--
Part of the Carbon Language project, under the Apache License v2.0 with LLVM
Exceptions. See /LICENSE for license information.
SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
-->
[Pull request](https://github.com/carbon-language/carbon-lang/pull/911)
<!-- toc -->
## Table of contents
- [Problem](#problem)
- [Background](#background)
- [Proposal](#proposal)
- [Details](#details)
- [Chaining](#chaining)
- [Precedence and ambiguity](#precedence-and-ambiguity)
- [Rationale based on Carbon's goals](#rationale-based-on-carbons-goals)
- [Alternatives considered](#alternatives-considered)
- [No conditional expression](#no-conditional-expression)
- [Use C syntax](#use-c-syntax)
- [No `then`](#no-then)
- [Require parentheses around the condition](#require-parentheses-around-the-condition)
- [Never require enclosing parentheses](#never-require-enclosing-parentheses)
- [Variable-precedence `if`](#variable-precedence-if)
- [Implicit conversions in both directions](#implicit-conversions-in-both-directions)
- [Support lvalue conditionals](#support-lvalue-conditionals)
- [Future work](#future-work)
- [Too many user-facing interfaces](#too-many-user-facing-interfaces)
- [Incompatible `CommonType` implementations diagnosed late](#incompatible-commontype-implementations-diagnosed-late)
- [`impl` ordering depends on operand order](#impl-ordering-depends-on-operand-order)
<!-- tocstop -->
## Problem
Programs need to be able to select between multiple different paths of execution
and multiple different values. In a rich expression language, developers expect
to be able to do this within a subexpression of some overall expression.
## Background
C-family languages provide a `cond ? value1 : value2` operator.
- This operator has confusing syntax, because both `cond` and `value2` are
undelimited, and it's often unclear to developers how much of the adjacent
expressions are part of the conditional expression. For example:
```
int n = has_thing1 && cond ? has_thing2 : has_thing3 && has_thing4;
```
is parsed as
```
int n = (has_thing1 && cond) ? has_thing2 : (has_thing3 && has_thing4);
```
Also, `value1` and `value2` are parsed with different rules:
```
cond ? f(), g() : h(), i();
```
is parsed as
```
(cond ? f(), g() : h()), i();
```
- In C++, this operator has confusing semantics, due to having a complicated
set of rules governing how the target type is determined.
- Despite the complications of the rules, the result type of `?:` is not
customizable. Instead, C++ invented a `std::common_type` trait that models
what the result of `?:` should have been.
Rust allows most statements to be used as expressions, with `if` statements
being an important case of this: `Use(if cond { v1 } else { v2 })`.
- This has a number of behaviors that would be surprising to developers coming
from C++ and C, such as a final `;` in a `{...}` making a semantic
difference.
- The expression semantics leak into the statement semantics. For example,
Rust rejects:
```
fn f() {}
fn g() -> i32 {}
fn main() {
if true { f() } else { g() };
return;
}
```
... because the two arms of the `if` don't have the same type.
- We have already
[decided](https://github.com/carbon-language/carbon-lang/issues/430) that we
do not want Carbon to treat statements such as `if` as being expressions
without some kind of syntactic distinction.
## Proposal
Provide a conditional expression with the syntax:
> ```
> if cond then value1 else value2
> ```
`then` is a new keyword introduced for this purpose.
## Details
### Chaining
This syntax can be chained like `if` statements:
```
Print(if guess < value
then "Too low!"
else if guess > value
then "Too high!"
else "Correct!")
```
Unlike with `if` statements, this doesn't require a special rule.
### Precedence and ambiguity
An `if` expression can be used as a top-level expression, or within parentheses
or a comma-separated list such as a function call. They have low precedence, so
cannot be used as the operand of any operator, with the exception of assignment
(if assignment is treated as an operator), but they can appear in other contexts
where an arbitrary expression is permitted, for example as the operand of
`return`, the initializer of a variable, or even as the condition of another
`if` expression or `if` statement.
```
// Error, can't use `if` here.
var v: i32 = 1 * if cond then 2 else 3 + 4;
```
`value2` extends as far to the right as possible:
```
var v: i32 = if cond then 2 else 3 + 4;
```
is the same as
```
var v: i32 = if cond then 2 else (3 + 4);
```
not
```
var v: i32 = (if cond then 2 else 3) + 4;
```
The intent is that an `if` expression is used to produce a value, not only for
its side-effects. If only the side-effects are desired, an `if` statement should
be used instead. Because `value2` extends as far to the right as possible, if an
`if` expression appeared at the start of a statement, its value could never be
used:
```
if cond then value1 else value2;
```
For this reason and to avoid the need for lookahead or disambiguation, an `if`
keyword appearing at the start of a statement is always interpreted as beginning
an `if` statement and never as beginning an `if` expression.
## Rationale based on Carbon's goals
- [Language tools and ecosystem](/docs/project/goals.md#language-tools-and-ecosystem)
- The `if ... then ... else` syntax should be easier to format
automatically in an unsurprising way than a `?:` syntax because it is
clear that the `then` and `else` keywords should be wrapped to the start
of a new line when wrapping the overall conditional expression.
- [Code that is easy to read, understand, and write](/docs/project/goals.md#code-that-is-easy-to-read-understand-and-write)
- Including such an expression is expected to improve ergonomics.
- An explicit delimiter for the start of the condition expression makes it
easier to read, correctly write, and understand the precedence of
conditional expressions.
- Making the `value2` portion as long as possible gives a simple rule that
it seems feasible for every Carbon developer to remember. This rule is
expected to be unsurprising both due to using the same rule for `value1`
and `value2`, and because it means that `if` consistently behaves like a
very low precedence prefix operator.
- The use of an explicit `if` keyword for flow control makes the
distinction between flow control and linear computation clearer.
- The readability of a multi-line `if` expression is improved by having a
`then` and `else` keyword of the same length
- [Interoperability with and migration from existing C++ code](/docs/project/goals.md#interoperability-with-and-migration-from-existing-c-code)
- Migration is improved by providing an operator set that largely matches
the C++ operator set.
## Alternatives considered
### No conditional expression
We could provide no conditional expression, and instead ask people to use a
different mechanism to achieve this functionality. Some options include:
- Use of an `if` statement:
```
var v: Result;
if (cond) {
v = value1;
} else {
v = value2;
}
Use(v);
```
- A function call syntax:
```
Use(cond.Select(value1, value2));
```
or, with short-circuiting and lambdas:
```
Use(cond.LazySelect($(value1), $(value2)));
```
- An `if` statement in a lambda:
```
Use(${ if (cond) { return value1; } else { return value2; } });
```
The above assumes a placeholder `$(...)` syntax for a single-expression lambda,
and a `${...}` syntax for a lambda with statements as its body.
Advantages:
- No new dedicated syntax.
Disadvantages:
- Conditional expressions are commonly used, commonly desired, and Carbon
developers -- especially those coming from C++ and C -- will be disappointed
by their absence.
- Readability and ergonomics will be harmed by making this common operation
more verbose, even if an idiom is established.
### Use C syntax
We could use the C `cond ? value1 : value2` syntax.
Advantages:
- Familiar to developers coming from C++ and C.
Disadvantages:
- These operators have serious precedence problems in C++ and C. We could
address those by making more cases ambiguous, at the cost of harming
familiarity and requiring parentheses in more cases.
- The `:` token is already in use in name binding; using it as part of a
conditional expression would be confusing.
- The `?` token is likely to be desirable for use in optional unwrapping and
error handling.
### No `then`
We could use
> ```
> if (cond) value1 else value2
> ```
instead of
> ```
> if cond then value1 else value2
> ```
Note that we cannot avoid parentheses in this formulation without risking
syntactic ambiguities.
Advantages:
- Looks more like an `if` statement, albeit one with unbraced operands.
- Slightly shorter.
- Better line-wrapping for chained `if` expressions:
```
Print(if (guess < value)
"Too low!"
else if (guess > value)
"Too high!"
else
"Correct!")
```
may be more readable than
```
Print(if guess < value
then "Too low!"
else if guess > value
then "Too high!"
else "Correct!")
```
or
```
Print(if guess < value
then "Too low!"
else if guess > value
then "Too high!"
else "Correct!")
```
Disadvantages:
- Potentially worse line wrapping. The `else` would presumably be wrapped onto
a line by itself, wasting vertical space, whereas `then` and `else` when
paired can both comfortably precede their values on the same line; consider
```
F(if (cond)
value1
else
value2)
```
occupies more space than
```
F(if cond
then value1
else value2)
```
- May create confusion between `if` statements and `if` expressions by
resembling an `if` statement but not matching the semantics.
- May cause evolutionary problems due to syntactic conflict if we ever make
the braces or parentheses in `if` statements optional.
- Requires parentheses, and hence additional presses of "Shift" on US
keyboards, making it slightly harder to type.
### Require parentheses around the condition
We could use:
> ```
> if (cond) then value1 else value2
> ```
However, it's not clear that there is value in requiring both parentheses and a
new keyword. It also seems jarring that this so closely resembles an `if`
statement but adds a `then` keyword that the `if` statement lacks.
### Never require enclosing parentheses
We could allow an `if` expression to appear anywhere a parenthesized expression
can appear, and retain the rule that `value2` extends as far to the right as
possible.
Advantages:
- Removes extra ceremony from a construct that is already more verbose than
the corresponding `?:` construct in C++.
- The requirement to include parentheses may be irritating in cases where
there is no other possible interpretation, such as
`1 + (if cond then 2 else 3)`.
Disadvantages:
- Visually ambiguous where `value2` ends in some cases, and violates
precedence rules.
- Hard for a simple yacc/bison parser to handle, due to ambiguity of `if` at
the start of a statement and ambiguity when parsing `value2`.
### Variable-precedence `if`
We could allow an `if` expression to appear anywhere a parenthesized expression
can appear, and parse the `value1` and `value2` as if they appeared in place of
the `if` expression:
```carbon
var n: i32 = 1 + if cond then 2 * 3 else 4 * 5 + 6;
// ... is interpreted as ...
var n: i32 = (1 + (if cond then (2 * 3) else (4 * 5))) + 6;
// Error: expected `else` but found `+ 4`.
var m: i32 = 1 + if cond then 2 * 3 + 4 else 5 + 6;
```
Advantages:
- Same as previous option.
Disadvantages:
- Confusing to readers, because it's not clear locally where the expression
after `else` ends, and discovering this requires looking backwards to before
the `if`.
- Hard for a simple yacc/bison parser to handle, due to needing at least one
production for an `if` statement for each precedence level. Also, those
productions will result in grammar ambiguities that will need to be
resolved.
### Implicit conversions in both directions
Suppose we have two types where implicit conversions in both directions are
possible:
```carbon
class A {}
class B {}
impl A as ImplicitAs(B) { ... }
impl B as ImplicitAs(A) { ... }
```
By default, an expression `if cond then {} as A else {} as B` would be
ambiguous. If the author of `A` or `B` wishes to change this behavior:
- If the common type should be `A`, then `impl A as CommonTypeWith(B)` must be
provided specifying the common type is `A`.
- If the common type should be `B`, then `impl B as CommonTypeWith(A)` must be
provided specifying the common type is `B`.
- If the common type should be something else, then both `impl`s need to be
provided:
```
impl A as CommonTypeWith(B) { let Result:! Type = C; }
impl B as CommonTypeWith(A) { let Result:! Type = C; }
```
We could change the rules so instead, in any of the above cases, implementing
either `A as CommonTypeWith(B)` or `B as CommonTypeWith(A)` would suffice.
Advantages:
- Simplifies the user experience in this case.
Disadvantages:
- Introduces non-uniformity: the blanket `impl` of `CommonTypeWith` in terms
of `ImplicitAs` would get this special treatment, but other blanket `impl`s
would not.
- Introduces complexity, which might not be fully hidden from users. At
minimum, we would need to explain that `ImplicitAs` is treated specially
here.
- The case in which two `impl`s are required is a corner case. It's somewhat
uncommon for implicit conversions to be possible in both directions between
two types. In those cases, it's more uncommon for there to be a clear best
"common type". And even then, most of the time the common type will be one
of the two types being unified.
From a more abstract perspective: the process of finding a common type involves
asking each type to implicitly convert to the destination type that it thinks is
best, and then failing if both sides didn't convert to the same type. If `A`
implicitly converts to `B` and the other way around, then both sides of this
process should be overridden in order to get both types to implicitly convert to
`C` instead.
### Support lvalue conditionals
Carbon doesn't formally have a notion of lvalue or rvalue yet; this notion is
expected to be added by
[#821: Values, variables, pointers, and references](https://github.com/carbon-language/carbon-lang/pull/821).
In any case, we certainly intend to distinguish between expressions that
represent values and expressions that represent locations where values could
appear. We therefore need to decide whether a conditional expression can ever be
in the latter category. For example:
```carbon
var a: String;
var b: String;
var c: bool;
// Valid?
(if c then a else b) = "Hello";
```
We could permit this, as C++ does. For example, we could say:
> If both _value1_ and _value2_ are lvalues then
> `if cond then value1 else value2` is rewritten to
> `*(if cond then &value1 else &value2)` if those pointer types have a common
> type.
The other reason we might want to consider this alternative is performance. In
C++, this code avoids making a `std::string` copy:
```c++
std::string a;
std::string b;
std::string c;
bool cond;
// ...
bool equal = c == (cond ? a : b);
```
... by treating the conditional expression as an lvalue of type `std::string`
rather than as a prvalue. However, in Carbon, following #821, we would expect
that the equivalent of a prvalue of type `std::string` would not necessarily
imply that a copy is made. Rather, Carbon's equivalent of prvalues would
represent either a set of instructions to initialize a value (as in C++), or the
location of some existing value that we are temporarily "borrowing".
With that in mind:
Advantages:
- More similar to C++.
- Permits certain operations that have an obvious intended meaning, such as
assignment to a conditional.
Disadvantages:
- Modification through an lvalue conditional is seldom used in C++, indicating
that this is not an important feature. The other benefits of a conditional
producing an lvalue are expected to be obtained by #821.
- Mutable inputs to operations ("out parameters") in Carbon are expected to be
expressed as pointers under #821, so there will be a `&` somewhere anyway;
given the choice between an lvalue conditional:
```
F(&(if cond then a else b));
```
and an rvalue-only conditional:
```
F(if cond then &a else &b);
```
the latter option would likely be preferred even if the former were
available.
- This would create an inconsistency in behavior, which would be particularly
visible in a generic when determining what constraints are necessary to
type-check an `if` expression -- the constraints would depend not only on
operand types, but also on value category, and may result in a hard to
express constraint such as "either `T*` and `U*` have a common type or `T`
and `U` have a common type".
- Certain kinds of lvalue conditional expression have turned out to be hard to
implement in C++, such as a conditional involving bit-field lvalues. We can
entirely avoid that class of implementation problems by treating conditional
expressions as rvalues.
This should be revisited if the direction in #821 changes substantially from the
assumptions described above.
## Future work
There are some known issues with the way that the extensibility mechanism works
in this proposal. It is hoped that extensions to Carbon's generics mechanism
will provide simple ways to resolve these issues. This design should be
revisited once those mechanisms are available.
### Too many user-facing interfaces
We provide both `CommonTypeWith`, as an extension point, and `CommonType`, as a
constraint. It would be preferable to provide only a single name that functions
both as the extension point and as a constraint, but we don't have a good way to
automatically make `impl`s symmetric and avoid `impl` cycles if we use only one
interface.
### Incompatible `CommonType` implementations diagnosed late
Example:
```
class A {}
class B {}
impl A as CommonTypeWith(B) where .Result = A {}
impl B as CommonTypeWith(A) where .Result = B {}
fn F(a: A, b: B) -> auto { return if true then a else b; }
```
The definition of function `F` is rejected, because `A` and `B` have no
(consistent) common type. It would be preferable to reject the `impl`
definitions.
### `impl` ordering depends on operand order
Example:
```
class A(T:! Type) {}
class B(T:! Type) {}
interface Fungible {}
impl A(T:! Type) as Fungible {}
impl B(T:! Type) as Fungible {}
// #1
impl A(T:! Type) as CommonTypeWith(U:! Fungible) where .Result = A(T) {}
// #2
impl B(T:! Type) as CommonTypeWith(A(T)) where .Result = T {}
fn F(a: A(i32), b: B(i32)) -> auto { return if true then a else b; }
```
Here, reversed #2 is a better match than #1, because it matches both `A(?)` and
`B(?)`, so #2 should be consider the best-matching `impl`. However, we never
compare reversed #2 against non-reversed #1. Instead, we look for:
1. `impl A(i32) as SymmetricCommonTypeWith(B(i32))`, which selects #1 as being
better than the blanket `impl` that reverses operand order.
2. `impl B(i32) as SymmetricCommonTypeWith(A(i32))`, which selects #2 as being
better than the blanket `impl` that reverses operand order.
So we decide that the `if` in `F` is ambiguous, even though there is a unique
best `CommonTypeWith` match. If either #1 or #2 is written with the operand
order reversed, then `F` would be accepted.