mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-09-24 19:10:14 +01:00
Implicit conversions (#820)
Proposal to support a limited set of implicit conversions. This would generally permit only implicit conversions that are lossless and semantics-preserving. In particular, this proposal allows: - Conversion from an integer type to a wider integer type of the same signedness, and from an unsigned integer type to a wider signed integer type. - Conversion from an integer type to a floating-point type that has enough mantissa bits to exactly represent all integers in the source type. - Conversion from integer literals to integer and floating-point types that can represent them. - Conversion from floating-point literals to floating-point types that can represent them. - Conversions required for generics: conversions of values between facet types, and conversions of types between type-of-types, as described in the generics proposals. - Conversions required for inheritance: derived-to-base conversions for class pointers and class values. Other conversions, such as lossy conversions between arithmetic types and conversions between bool and other types are not supported. Co-authored-by: josh11b <josh11b@users.noreply.github.com> Co-authored-by: Chandler Carruth <chandlerc@gmail.com> Co-authored-by: Geoff Romer <gromer@google.com>
This commit is contained in:
committed by
GitHub
co-authored by
josh11b
Chandler Carruth
Geoff Romer
parent
7beaabc934
commit
f63169608e
@@ -0,0 +1,44 @@
|
||||
# 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)
|
||||
- [Implicit conversions](#implicit-conversions)
|
||||
|
||||
<!-- tocstop -->
|
||||
|
||||
## Overview
|
||||
|
||||
Expressions are the portions of Carbon syntax that produce values. Because types
|
||||
in Carbon are values, this includes anywhere that a type is specified.
|
||||
|
||||
```
|
||||
fn Foo(a: i32*) -> i32 {
|
||||
return *a;
|
||||
}
|
||||
```
|
||||
|
||||
Here, the parameter type `i32*`, the return type `i32`, and the operand `*a` of
|
||||
the `return` statement are all expressions.
|
||||
|
||||
## Implicit conversions
|
||||
|
||||
When an expression appears in a context in which an expression of a specific
|
||||
type is expected, [implicit conversions](implicit_conversions.md) are applied to
|
||||
convert the expression to the target type.
|
||||
|
||||
```
|
||||
fn Bar(n: i32);
|
||||
fn Baz(n: i64) {
|
||||
// OK, same as Bar(n as i32)
|
||||
Bar(n);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,297 @@
|
||||
# Implicit conversions
|
||||
|
||||
<!--
|
||||
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)
|
||||
- [Properties of implicit conversions](#properties-of-implicit-conversions)
|
||||
- [Lossless](#lossless)
|
||||
- [Semantics-preserving](#semantics-preserving)
|
||||
- [Examples](#examples)
|
||||
- [Built-in types](#built-in-types)
|
||||
- [Data types](#data-types)
|
||||
- [Equivalent types](#equivalent-types)
|
||||
- [Pointer conversions](#pointer-conversions)
|
||||
- [Pointer conversion examples](#pointer-conversion-examples)
|
||||
- [Type-of-types](#type-of-types)
|
||||
- [Semantics](#semantics)
|
||||
- [Extensibility](#extensibility)
|
||||
- [Alternatives considered](#alternatives-considered)
|
||||
- [References](#references)
|
||||
|
||||
<!-- tocstop -->
|
||||
|
||||
## Overview
|
||||
|
||||
When an expression appears in a context in which an expression of a specific
|
||||
type is expected, the expression is implicitly converted to that type if
|
||||
possible.
|
||||
|
||||
For [built-in types](#built-in-types), implicit conversions are permitted when:
|
||||
|
||||
- The conversion is [_lossless_](#lossless): every possible value for the
|
||||
source expression converts to a distinct value in the target type.
|
||||
- The conversion is [_semantics-preserving_](#semantics-preserving):
|
||||
corresponding values in the source and destination type have the same
|
||||
abstract meaning.
|
||||
|
||||
These rules aim to ensure that implicit conversions are unsurprising: the value
|
||||
that is provided as the operand of an operation should match how that operation
|
||||
interprets the value, because the identity and abstract meaning of the value are
|
||||
preserved by any implicit conversions that are applied.
|
||||
|
||||
It is possible for user-defined types to [extend](#extensibility) the set of
|
||||
valid implicit conversions. Such extensions are expected to also follow these
|
||||
rules.
|
||||
|
||||
## Properties of implicit conversions
|
||||
|
||||
### Lossless
|
||||
|
||||
We expect implicit conversion to never lose information: if two values are
|
||||
distinguishable before the conversion, they should generally be distinguishable
|
||||
after the conversion. It should be possible to define a conversion in the
|
||||
opposite direction that restores the original value, but such a conversion is
|
||||
not expected to be provided in general, and might be computationally expensive.
|
||||
|
||||
Because an implicit conversion is converting from a narrower type to a wider
|
||||
type, implicit conversions do not necessarily preserve static information about
|
||||
the source value.
|
||||
|
||||
### Semantics-preserving
|
||||
|
||||
We expect implicit conversions to preserve the meaning of converted values. The
|
||||
assessment of this criterion will necessarily be subjective, because the
|
||||
meanings of values generally live in the mind of the programmer rather than in
|
||||
the program text. However, the semantic interpretation is expected to be
|
||||
consistent from one conversion to another, so we can provide a test: if multiple
|
||||
paths of implicit conversions from a type `A` to a type `B` exist, and the same
|
||||
value of type `A` would convert to different values of type `B` along different
|
||||
paths, then at least one of those conversions must not be semantics-preserving.
|
||||
|
||||
A semantics-preserving conversion does not necessarily preserve the meaning of
|
||||
particular syntax when applied to the value. The same syntax may map to
|
||||
different operations in the new type. For example, division may mean different
|
||||
things in integer and floating-point types, and member access may find different
|
||||
members in a derived class pointer versus in a base class pointer.
|
||||
|
||||
### Examples
|
||||
|
||||
Conversion from `i32` to `Vector(int)` by forming a vector of N zeroes is
|
||||
lossless but not semantics-preserving.
|
||||
|
||||
Conversion from `i32` to `f32` by rounding to the nearest representable value is
|
||||
semantics-preserving but not lossless.
|
||||
|
||||
Conversion from `String` to `StringView` is lossless, because we can compute the
|
||||
`String` value from the `StringView` value, and semantics-preserving because the
|
||||
string value denoted is the same. Conversion in the other direction may or may
|
||||
not be semantics-preserving depending on whether we consider the address to be a
|
||||
salient part of a `StringView`'s value.
|
||||
|
||||
## Built-in types
|
||||
|
||||
### Data types
|
||||
|
||||
The following implicit numeric conversions are available:
|
||||
|
||||
- `iN` or `uN` -> `iM` if `M` > `N`
|
||||
- `uN` -> `uM` if `M` > `N`
|
||||
- `fN` -> `fM` if `M` > `N`
|
||||
- `iN` or `uN` -> `fM` if every value of type `iN` or `uN` can be represeted
|
||||
in `fM`:
|
||||
- `i12` or `u11` (or smaller) -> `f16`
|
||||
- `i25` or `u24` (or smaller) -> `f32`
|
||||
- `i54` or `u53` (or smaller) -> `f64`
|
||||
- `i65` or `u64` (or smaller) -> `f80` (x86 only)
|
||||
- `i114` or `u113` (or smaller) -> `f128` (if available)
|
||||
- `i238` or `u237` (or smaller) -> `f256` (if available)
|
||||
|
||||
In each case, the numerical value is the same before and after the conversion.
|
||||
An integer zero is translated into a floating-point positive zero.
|
||||
|
||||
An integer constant can be implicitly converted to any type `iM`, `uM`, or `fM`
|
||||
in which that value can be exactly represented. A floating-point constant can be
|
||||
implicitly converted to any type `fM` in which that value is between the least
|
||||
representable finite value and the greatest representable finite value
|
||||
(inclusive), and does not fall exactly half-way between two representable
|
||||
values, and converts to the nearest representable finite value.
|
||||
|
||||
The above conversions are also precisely those that C++ considers non-narrowing,
|
||||
except:
|
||||
|
||||
- Carbon also permits integer to floating-point conversions in more cases. The
|
||||
most important of these is that Carbon permits `i32` to be implicitly
|
||||
converted to `f64`. Lossy conversions, such as from `i32` to `f32`, are not
|
||||
permitted.
|
||||
|
||||
- What Carbon considers to be an integer constant or floating-point constant
|
||||
may differ from what C++ considers to be a constant expression.
|
||||
|
||||
**Note:** We have not yet decided what will qualify as a constant in this
|
||||
context, but it will include at least integer and floating-point literals,
|
||||
with optional enclosing parentheses. It is possible that such constants will
|
||||
have singleton types; see issue
|
||||
[#508](https://github.com/carbon-language/carbon-lang/issues/508).
|
||||
|
||||
### Equivalent types
|
||||
|
||||
The following conversion is available:
|
||||
|
||||
- `T` -> `U` if `T` is equivalent to `U`
|
||||
|
||||
Two types are equivalent if they can be used interchangeably, implicitly: they
|
||||
have the same set of values with the same meaning and the same representation,
|
||||
with the same set of capabilities and constraints, where the only difference is
|
||||
how the type interprets operations on values of that type.
|
||||
|
||||
`T` is equivalent to `U` if:
|
||||
|
||||
- `T` is the same type as `U`, or
|
||||
- `T` is the facet type `U as SomeInterface`, or
|
||||
- `U` is the facet type `T as SomeInterface`, or
|
||||
- `T` is `A*`, `U` is `B*`, and `A` is equivalent to `B`, or
|
||||
- for some type `V`, `T` is equivalent to `V` and `V` is equivalent to `U`.
|
||||
|
||||
**Note:** More type equivalence rules are expected to be added over time.
|
||||
|
||||
A prerequisite for types being equivalent is that they are
|
||||
[compatible](../generics/terminology.md#compatible-types), and in particular
|
||||
that they have the same set of values and the same representation for those
|
||||
values. However, types being compatible does not imply that an implicit
|
||||
conversion, or even an explicit cast, between those types is necessarily valid.
|
||||
This is because the type of a value models not only the representation of the
|
||||
value but also the capabilities that a user of the value has to interact with
|
||||
the value. Two compatible types may expose different capabilities, such as the
|
||||
capability to mutate the object or to access its implementation details, and
|
||||
conversions between such types may require an explicit cast if the conversion is
|
||||
possible at all.
|
||||
|
||||
### Pointer conversions
|
||||
|
||||
The following pointer conversion is available:
|
||||
|
||||
- `T*` -> `U*` if `T` is a subtype of `U`.
|
||||
|
||||
`T` is a subtype of `U` if:
|
||||
|
||||
- `T` is equivalent to `U`, as described above, or
|
||||
- `T` is equivalent to a class derived from a class equivalent to `U`.
|
||||
|
||||
**Note:** More type subtyping rules are expected to be added over time.
|
||||
|
||||
`T*` is not necessarily a subtype of `U*` even if `T` is a subtype of `U`. For
|
||||
example, we can convert `Derived*` to `Base*`, but cannot convert `Derived**` to
|
||||
`Base**` because that would allow storing a `Derived2*` into a `Derived*`:
|
||||
|
||||
```
|
||||
abstract class Base {}
|
||||
class Derived extends Base {}
|
||||
class Derived2 extends Base {}
|
||||
var d2: Derived2 = {};
|
||||
var p: Derived*;
|
||||
var q: Derived2* = &d2;
|
||||
var r: Base** = &p;
|
||||
// Bad: would store q to p.
|
||||
*r = q;
|
||||
```
|
||||
|
||||
**Note:** If we add `const` qualification, we could treat `const T*` as a
|
||||
subtype of `const U*` if `T` is a subtype of `U`, and could treat `T` as a
|
||||
subtype of `const T`.
|
||||
|
||||
#### Pointer conversion examples
|
||||
|
||||
With these classes:
|
||||
|
||||
```
|
||||
base class C;
|
||||
let F: auto = C as Hashable;
|
||||
class D extends C;
|
||||
```
|
||||
|
||||
These implicit pointer conversions are permitted:
|
||||
|
||||
- `D*` -> `C*`: `D` is a subtype of `C`
|
||||
- `F*` -> `C*`: `F` is equivalent to `C`, so `F` is a subtype of `C`
|
||||
- `C*` -> `F*`: `C` is equivalent to `F`, so `C` is a subtype of `F`
|
||||
- `F**` -> `C**`: `F` is equivalent to `C`, so `F*` is equivalent to `C*`, so
|
||||
`F*` is a subtype of `C*`
|
||||
- `D*` -> `F*`: `D` is derived from `C` and `C` is equivalent to `D`, so `D`
|
||||
is a subtype of `F`
|
||||
|
||||
These implicit pointer conversions are disallowed:
|
||||
|
||||
- `C*` -> `D*`: `C` is not a subtype of `D`
|
||||
- `D**` -> `C**`: `D*` is not a subtype of `C*`
|
||||
|
||||
Note that "equivalent to" means we can freely convert back and forwards; the
|
||||
difference in the types is just changing which operations are surfaced, not
|
||||
changing anything about the interpretation or switching between different
|
||||
abstractions. In contrast, "subtype of" permits conversion from a more specific
|
||||
type to a more general type, so the reverse conversion is not necessarily valid.
|
||||
|
||||
### Type-of-types
|
||||
|
||||
A type `T` with [type-of-type](../generics/terminology.md#type-of-type) `TT1`
|
||||
can be implicitly converted to the type-of-type `TT2` if `T`
|
||||
[satisfies the requirements](../generics/details.md#subtyping-between-type-of-types)
|
||||
of `TT2`.
|
||||
|
||||
## Semantics
|
||||
|
||||
An implicit conversion of an expression `E` of type `T` to type `U`, when
|
||||
permitted, always has the same meaning as the explicit cast expression `E as U`.
|
||||
Moreover, such an implicit conversion is expected to exactly preserve the value.
|
||||
For example, `(E as U) as T`, if valid, should be expected to result in the same
|
||||
value as produced by `E`.
|
||||
|
||||
**Note:** The explicit cast expression syntax has not yet been decided. The use
|
||||
of `E as T` in this document is provisional.
|
||||
|
||||
## Extensibility
|
||||
|
||||
Implicit conversions can be defined for user-defined types such as
|
||||
[classes](../classes.md) by implementing the `ImplicitAs` interface:
|
||||
|
||||
```
|
||||
interface As(Dest:! Type) {
|
||||
fn Convert[me: Self]() -> Dest;
|
||||
}
|
||||
interface ImplicitAs(Dest:! Type) extends As(Dest) {}
|
||||
```
|
||||
|
||||
When attempting to implicitly convert an expression `x` to type `U`, the
|
||||
expression is rewritten to `x.(ImplicitAs(U).Convert)()`.
|
||||
|
||||
**Note:** The `As` interface is intended to be used as the implementation
|
||||
vehicle for explicit casts: `x as U` would be rewritten as
|
||||
`x.(As(U).Convert)()`. However, the explicit cast expression syntax has not yet
|
||||
been decided, so this rewrite is provisional.
|
||||
|
||||
Note that implicit conversions are not transitive. Even if an
|
||||
`impl A as ImplicitAs(B)` and an `impl B as ImplicitAs(C)` are both provided, an
|
||||
expression of type `A` cannot be implicitly converted to type `C`. Allowing
|
||||
transitivity would introduce the risk of ambiguity issues as code evolves and
|
||||
would in general require a search of a potentially unbounded set of intermediate
|
||||
types.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- [Provide lossy and non-semantics-preserving implicit conversions from C++](/docs/proposals/p0820.md#c-conversions)
|
||||
- [Provide no implicit conversions](/docs/proposals/p0820.md#no-conversions)
|
||||
- [Provide no extensibility](/docs/proposals/p0820.md#no-extensibility)
|
||||
- [Apply implicit conversions transitively](/docs/proposals/p0820.md#transitivity)
|
||||
|
||||
## References
|
||||
|
||||
- [Implicit conversions in C++](https://en.cppreference.com/w/cpp/language/implicit_conversion)
|
||||
- Proposal
|
||||
[#820: implicit conversions](https://github.com/carbon-language/carbon-lang/pull/820).
|
||||
@@ -144,7 +144,7 @@ properties:
|
||||
just implementations of the names and signatures defined in the
|
||||
`ConvertibleToString` interface, like `ToString`, and not the functions
|
||||
defined on `Song` values.
|
||||
- Carbon will implicitly cast values from type `Song` to type
|
||||
- Carbon will implicitly convert values from type `Song` to type
|
||||
`Song as ConvertibleToString` when calling a function that can only accept
|
||||
types of type `ConvertibleToString`.
|
||||
- In the normal case where the implementation of `ConvertibleToString` for
|
||||
@@ -172,7 +172,7 @@ guaranteed to see when needed. For more on this, see
|
||||
If `Song` doesn't implement an interface or we would like to use a different
|
||||
implementation of that interface, we can define another type that also has the
|
||||
same data representation as `Song` that has whatever different interface
|
||||
implementations we want. However, Carbon won't implicitly cast to that other
|
||||
implementations we want. However, Carbon won't implicitly convert to that other
|
||||
type, the user will have to explicitly cast to that type in order to select
|
||||
those alternate implementations. For more on this, see
|
||||
[the adapting type section](#adapting-types) below.
|
||||
@@ -259,8 +259,8 @@ The `impl` definition defines a [facet type](terminology.md#facet-type):
|
||||
along with the `Add` and `Scale` methods, the API of `Point as Vector` _only_
|
||||
has the `Add` and `Scale` methods of the `Vector` interface. The facet type
|
||||
`Point as Vector` is [compatible](terminology.md#compatible-types) with `Point`,
|
||||
meaning their data representations are the same, so we allow you to cast between
|
||||
the two freely:
|
||||
meaning their data representations are the same, so we allow you to convert
|
||||
between the two freely:
|
||||
|
||||
```
|
||||
var a: Point = (.x = 1.0, .y = 2.0);
|
||||
@@ -272,7 +272,7 @@ var b: Point as Vector = a;
|
||||
// `b` has `Add` and `Scale` methods:
|
||||
b.Add(b.Scale(2.0));
|
||||
|
||||
// Will also implicitly cast when calling functions:
|
||||
// Will also implicitly convert when calling functions:
|
||||
fn F(c: Point as Vector, d: Point) {
|
||||
d.Add(c.Scale(2.0));
|
||||
}
|
||||
@@ -284,7 +284,7 @@ z.Add(b);
|
||||
var w: Point = z as Point;
|
||||
```
|
||||
|
||||
These [casts](terminology.md#subtyping-and-casting) change which names are
|
||||
These [conversions](terminology.md#subtyping-and-casting) change which names are
|
||||
exposed in the type's API, but as much as possible we don't want the meaning of
|
||||
any given name to change. Instead we want these casts to simply change the
|
||||
subset of names that are visible.
|
||||
@@ -300,8 +300,8 @@ of selecting an implementation of an interface for a type unambiguous throughout
|
||||
the whole program, so for example `Point as Vector` is well defined.
|
||||
|
||||
We don't expect users to ordinarily name facet types explicitly in source code.
|
||||
Instead, values are implicitly cast to a facet type as part of calling a generic
|
||||
function, as described in the [Generics](#generics) section.
|
||||
Instead, values are implicitly converted to a facet type as part of calling a
|
||||
generic function, as described in the [Generics](#generics) section.
|
||||
|
||||
### Implementing multiple interfaces
|
||||
|
||||
@@ -405,7 +405,7 @@ can find all the names of direct (unqualified) members of a type in the
|
||||
definition of that type. The only thing that may be in another library is an
|
||||
`impl` of an interface.
|
||||
|
||||
On the other hand, if we cast to the facet type, those methods do become
|
||||
On the other hand, if we convert to the facet type, those methods do become
|
||||
visible:
|
||||
|
||||
```
|
||||
@@ -413,7 +413,7 @@ var a: Point2 = (.x = 1.0, .y = 2.0);
|
||||
// `a` does *not* have `Add` and `Scale` methods:
|
||||
// ❌ Error: a.Add(a.Scale(2.0));
|
||||
|
||||
// Cast from Point2 implicitly
|
||||
// Convert from Point2 implicitly
|
||||
var b: Point2 as Vector = a;
|
||||
// `b` does have `Add` and `Scale` methods:
|
||||
b.Add(b.Scale(2.0));
|
||||
@@ -552,8 +552,8 @@ fn AddAndScaleForPointAsVector(
|
||||
-> Point as Vector {
|
||||
return a.Add(b).Scale(s);
|
||||
}
|
||||
// May still be called with Point arguments, due to implicit casts.
|
||||
// Similarly the return value can be implicitly cast to a Point.
|
||||
// May still be called with Point arguments, due to implicit conversions.
|
||||
// Similarly the return value can be implicitly converted to a Point.
|
||||
var v2: Point = AddAndScaleForPointAsVector(a, w, 2.5);
|
||||
```
|
||||
|
||||
@@ -697,10 +697,11 @@ An interface is one particularly simple example of a type-of-type. For example,
|
||||
interface `Vector`. Its set of names consists of `Add` and `Scale` which are
|
||||
aliases for the corresponding qualified names inside `Vector` as a namespace.
|
||||
|
||||
The requirements determine which types may be cast to a given type-of-type. The
|
||||
result of casting a type `T` to a type-of-type `I` (written `T as I`) is called
|
||||
a facet type, you might say a facet type `F` is the `I` facet of `T` if `F` is
|
||||
`T as I`. The API of `F` is determined by the set of names in the type-of-type.
|
||||
The requirements determine which types may be converted to a given type-of-type.
|
||||
The result of converting a type `T` to a type-of-type `I` (written `T as I`) is
|
||||
called a facet type, you might say a facet type `F` is the `I` facet of `T` if
|
||||
`F` is `T as I`. The API of `F` is determined by the set of names in the
|
||||
type-of-type.
|
||||
|
||||
This general structure of type-of-types holds not just for interfaces, but
|
||||
others described in the rest of this document.
|
||||
@@ -820,10 +821,10 @@ one generic function from another as long as you are calling a function with a
|
||||
subset of your requirements.
|
||||
|
||||
Given a generic type `T` with type-of-type `I1`, it may be
|
||||
[implicitly cast](terminology.md#subtyping-and-casting) to a type-of-type `I2`,
|
||||
resulting in `T as I2`, as long as the requirements of `I1` are a superset of
|
||||
the requirements of `I2`. Further, given a value `x` of type `T`, it can be
|
||||
implicitly cast to `T as I2`. For example:
|
||||
[implicitly converted](../expressions/implicit_conversions.md) to a type-of-type
|
||||
`I2`, resulting in `T as I2`, as long as the requirements of `I1` are a superset
|
||||
of the requirements of `I2`. Further, given a value `x` of type `T`, it can be
|
||||
implicitly converted to `T as I2`. For example:
|
||||
|
||||
```
|
||||
interface Printable { fn Print[me: Self](); }
|
||||
@@ -1384,10 +1385,10 @@ requiring an implementation of interface `B` means `A` is more specific than
|
||||
|
||||
## Type compatibility
|
||||
|
||||
None of the casts between facet types change the implementation of any
|
||||
interfaces for a type. So the result of a cast does not depend on the sequence
|
||||
of casts you perform, just the original type and the final type-of-type. That
|
||||
is, these types will all be equal:
|
||||
None of the conversions between facet types change the implementation of any
|
||||
interfaces for a type. So the result of a conversion does not depend on the
|
||||
sequence of conversions you perform, just the original type and the final
|
||||
type-of-type. That is, these types will all be equal:
|
||||
|
||||
- `T as I`
|
||||
- `(T as A) as I`
|
||||
@@ -1475,8 +1476,8 @@ This allows us to provide implementations of new interfaces (as in
|
||||
is an equivalence class, so all of `Song`, `SongByTitle`, `FormattedSong`,
|
||||
and `FormattedSongByTitle` end up compatible with each other.
|
||||
- Since adapted types are compatible with the original type, you may
|
||||
explicitly cast between them, but there is no implicit casting between these
|
||||
types (unlike between a type and one of its facet types / impls).
|
||||
explicitly cast between them, but there is no implicit conversion between
|
||||
these types (unlike between a type and one of its facet types / impls).
|
||||
- For the purposes of generics, we only need to support adding interface
|
||||
implementations. But this `adapter` feature could be used more generally,
|
||||
such as to add methods.
|
||||
@@ -1545,7 +1546,7 @@ compiler provides it as
|
||||
### Adapter compatibility
|
||||
|
||||
The framework from the [type compatibility section](#type-compatibility) allows
|
||||
us to evaluate when we can cast between two different arguments to a
|
||||
us to evaluate when we can convert between two different arguments to a
|
||||
parameterized type. Consider three compatible types, all of which implement
|
||||
`Hashable`:
|
||||
|
||||
@@ -1569,10 +1570,10 @@ Observe that `Song as Hashable` is different from
|
||||
`Song as Hashable` and `PlayableSong as Hashable` are almost the same. In
|
||||
addition to using the same data representation, they both implement one
|
||||
interface, `Hashable`, and use the same implementation for that interface. The
|
||||
one difference between them is that `Song as Hashable` may be implicitly cast to
|
||||
`Song`, which implements interface `Printable`, and `PlayableSong as Hashable`
|
||||
may be implicilty cast to `PlayableSong`, which implements interface `Media`.
|
||||
This means that it is safe to cast between
|
||||
one difference between them is that `Song as Hashable` may be implicitly
|
||||
converted to `Song`, which implements interface `Printable`, and
|
||||
`PlayableSong as Hashable` may be implicilty converted to `PlayableSong`, which
|
||||
implements interface `Media`. This means that it is safe to convert between
|
||||
`HashMap(Song, Int) == HashMap(Song as Hashable, Int)` and
|
||||
`HashMap(PlayableSong, Int) == HashMap(PlayableSong as Hashable, Int)` (though
|
||||
maybe only with an explicit cast) but
|
||||
@@ -1645,7 +1646,7 @@ adapter Song extends SongLib.Song { }
|
||||
external impl Song as CompareLib.Comparable { ... }
|
||||
```
|
||||
|
||||
The caller can either cast `SongLib.Song` values to `Song` when calling
|
||||
The caller can either convert `SongLib.Song` values to `Song` when calling
|
||||
`CompareLib.Sort` or just start with `Song` values in the first place.
|
||||
|
||||
```
|
||||
|
||||
@@ -371,11 +371,16 @@ its type as reflected in the API available to manipulate the value.
|
||||
|
||||
Casting is indicated explicitly by way of some syntax in the source code. You
|
||||
might use a cast to switch between [type adaptations](#adapting-a-type), or to
|
||||
be explicit where an implicit cast would otherwise occur. For now, we are saying
|
||||
"`x as y`" is the provisional syntax in Carbon for casting the value `x` to the
|
||||
type `y`. Note that outside of generics, the term "casting" includes any
|
||||
be explicit where an implicit conversion would otherwise occur. For now, we are
|
||||
saying "`x as y`" is the provisional syntax in Carbon for casting the value `x`
|
||||
to the type `y`. Note that outside of generics, the term "casting" includes any
|
||||
explicit type change, including those that change the data representation.
|
||||
|
||||
In contexts where an expression of one type is provided and a different type is
|
||||
required, an [implicit conversion](../expressions/implicit_conversions.md) is
|
||||
performed if it is considered safe to do so. Such an implicit conversion, if
|
||||
permitted, always has the same meaning as an explicit cast.
|
||||
|
||||
## Adapting a type
|
||||
|
||||
A type can be adapted by creating a new type that is
|
||||
|
||||
@@ -71,6 +71,7 @@ request:
|
||||
- [0731 - Generics details 2: adapters, associated types, parameterized interfaces](p0731.md)
|
||||
- [0752 - `api` file default-`public`](p0752.md)
|
||||
- [0777 - Inheritance](p0777.md)
|
||||
- [0820 - Implicit conversions](p0820.md)
|
||||
- [0829 - One way principle](p0829.md)
|
||||
|
||||
<!-- endproposals -->
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
# Implicit conversions
|
||||
|
||||
<!--
|
||||
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/820)
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Problem](#problem)
|
||||
- [Background](#background)
|
||||
- [Proposal](#proposal)
|
||||
- [Rationale based on Carbon's goals](#rationale-based-on-carbons-goals)
|
||||
- [Alternatives considered](#alternatives-considered)
|
||||
- [C++ conversions](#c-conversions)
|
||||
- [Array-to-pointer conversions](#array-to-pointer-conversions)
|
||||
- [Function-to-pointer conversions](#function-to-pointer-conversions)
|
||||
- [Qualification conversions](#qualification-conversions)
|
||||
- [Integral promotions](#integral-promotions)
|
||||
- [Floating-point promotions](#floating-point-promotions)
|
||||
- [Integral conversions](#integral-conversions)
|
||||
- [Floating-point conversions](#floating-point-conversions)
|
||||
- [Pointer conversions](#pointer-conversions)
|
||||
- [Pointer-to-member conversions](#pointer-to-member-conversions)
|
||||
- [Function pointer conversions](#function-pointer-conversions)
|
||||
- [Boolean conversions](#boolean-conversions)
|
||||
- [No conversions](#no-conversions)
|
||||
- [No extensibility](#no-extensibility)
|
||||
- [Transitivity](#transitivity)
|
||||
|
||||
<!-- tocstop -->
|
||||
|
||||
## Problem
|
||||
|
||||
Frequently, an expression provided as input to an operation has a type that does
|
||||
not exactly match the expected type. To improve the language ergonomics, we do
|
||||
not want to require explicit conversions in all such cases. However, there is
|
||||
strong evidence from C++ that allowing certain kinds of implicit conversion is
|
||||
dangerous and harmful in practice. We need to find a reasonable balance.
|
||||
|
||||
## Background
|
||||
|
||||
C++ permits many kinds of implicit conversion, some of which are generally
|
||||
considered good, and others are sometimes harmful. For example:
|
||||
|
||||
- `int` implicitly converts to `long`. This is useful and seldom harmful.
|
||||
- `long` implicitly converts to `int` and to `unsigned int`. This can result
|
||||
in data loss.
|
||||
- `int*` implicitly converts to `bool`. This can be useful in some contexts,
|
||||
such as `if (p)`, but surprising and harmful in others.
|
||||
|
||||
See also
|
||||
[implicit conversions in C++](https://en.cppreference.com/w/cpp/language/implicit_conversion).
|
||||
|
||||
## Proposal
|
||||
|
||||
See changes to design.
|
||||
|
||||
## Rationale based on Carbon's goals
|
||||
|
||||
- [Software and language evolution](/docs/project/goals.md#software-and-language-evolution)
|
||||
- Disallowing implicit conversions that lose information reduces the risk
|
||||
that existing code will be reinterpreted in a harmful way as libraries
|
||||
in use evolve.
|
||||
- [Code that is easy to read, understand, and write](/docs/project/goals.md#code-that-is-easy-to-read-understand-and-write)
|
||||
- Permitting a limited, safe set of implicit conversions reduces the
|
||||
boilerplate work necessary to write code.
|
||||
- Generics rely on performing implicit conversions between different
|
||||
type-of-types for deduced type parameters. Applying the same rules
|
||||
consistently for all expressions makes the language simpler.
|
||||
- [Interoperability with and migration from existing C++ code](/docs/project/goals.md#interoperability-with-and-migration-from-existing-c-code)
|
||||
- Providing some of the same implicit conversions as C++ reduces the need
|
||||
to add explicit casts when migrating. However, explicit casts will still
|
||||
be required when the C++ code was performing an operation that we don't
|
||||
consider safe.
|
||||
- Support for implicit conversions provides a path to expose converting
|
||||
constructors and conversion functions defined in C++ code to Carbon.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
### C++ conversions
|
||||
|
||||
We could permit more of the conversions that C++ does. This section considers
|
||||
each kind of implicit conversion in C++ and provides a description of the
|
||||
deviation and a rationale.
|
||||
|
||||
#### Array-to-pointer conversions
|
||||
|
||||
Array types have not yet been designed yet, so this is out of scope for now.
|
||||
|
||||
One possible design would be for pointers to not support arithmetic, and for
|
||||
arrays to provide "array iterators" that do supply such arithmetic. In this
|
||||
design, an implicit conversion from arrays to array iterators would likely be
|
||||
surprising.
|
||||
|
||||
#### Function-to-pointer conversions
|
||||
|
||||
Function pointer types have not been designed yet, and might not exist in the
|
||||
same form as in C++, so this is out of scope for now.
|
||||
|
||||
One possible design would be to have no function pointer types, and instead
|
||||
model functions as values of a unique type that implements a certain `Callable`
|
||||
interface. Then a function pointer could be modeled as a type-erased generic
|
||||
implementing `Callable`. In this model, there would be an implicit conversion
|
||||
from a function value to such a type-erased generic value.
|
||||
|
||||
#### Qualification conversions
|
||||
|
||||
So far, Carbon has no notion of cv-qualification. However, these conversions
|
||||
would likely be covered by the permission to convert from `T*` to `U*` if `T` is
|
||||
a subtype of `U`.
|
||||
|
||||
#### Integral promotions
|
||||
|
||||
Carbon disallows implicit conversion from `bool` to integral types. We could
|
||||
permit such implicit conversions.
|
||||
|
||||
Advantages:
|
||||
|
||||
- Improves C++ compatibility.
|
||||
- Permits constructs to count how many of a set of predicates were true:
|
||||
`if (cond1 + cond2 + cond3 >= 2)`.
|
||||
|
||||
Disadvantages:
|
||||
|
||||
- Treating truth values as the integers 0 and 1 results in code that is harder
|
||||
to read and understand.
|
||||
- This conversion can result in unexpected overloads being called, when a
|
||||
`bool` argument is passed to a parameter of some other type.
|
||||
|
||||
#### Floating-point promotions
|
||||
|
||||
This conversion is permitted.
|
||||
|
||||
#### Integral conversions
|
||||
|
||||
These conversions are only permitted when they are known to preserve the
|
||||
original value. These are the conversions that are considered non-narrowing in
|
||||
C++.
|
||||
|
||||
We could permit narrowing integer conversions.
|
||||
|
||||
Advantages:
|
||||
|
||||
- Improves C++ compatibility.
|
||||
- Allows implicitly undoing implicit widening in constructs such as
|
||||
`char n; char c = '0' + n;` where C++ promotes `'0' + n` to `int`.
|
||||
- However, Carbon is unlikely to implicitly widen to `i32` here.
|
||||
|
||||
Disadvantages:
|
||||
|
||||
- Introduces the potential for implicit data loss.
|
||||
|
||||
#### Floating-point conversions
|
||||
|
||||
Carbon disallows implicit conversion from a more-precise floating-point type to
|
||||
a less-precise floating-point type, such as from `f64` to `f32`. We could permit
|
||||
these implicit conversions.
|
||||
|
||||
Advantages:
|
||||
|
||||
- Improves C++ compatibility.
|
||||
- Allows implicitly undoing implicit widening in constructs such as
|
||||
`float a, b; float c = a + b;` where C++ promotes `a + b` to `double`.
|
||||
- However, Carbon might not implicitly widen to `f64` here.
|
||||
|
||||
Disadvantages:
|
||||
|
||||
- Introduces the potential for implicit loss of precision.
|
||||
- Introduces the risk that a low-precision operation might be selected when
|
||||
given higher-precision operands.
|
||||
|
||||
#### Pointer conversions
|
||||
|
||||
Carbon permits the equivalent conversions, except for the conversion from
|
||||
`nullptr` to pointer type. We anticipate that Carbon pointers will not be
|
||||
nullable by default.
|
||||
|
||||
Once nullable pointers are designed, we would expect an expression representing
|
||||
the null state would be implicitly convertible to the nullable pointer type.
|
||||
|
||||
#### Pointer-to-member conversions
|
||||
|
||||
Carbon does not yet have pointer-to-member types. This is out of scope for now.
|
||||
|
||||
#### Function pointer conversions
|
||||
|
||||
Carbon does not yet have function pointer types. This is out of scope for now.
|
||||
|
||||
#### Boolean conversions
|
||||
|
||||
An implicit conversion from arithmetic types and pointer types to `bool` is not
|
||||
provided. Pointer types are expected to not be nullable by default, so that part
|
||||
is out of scope for now.
|
||||
|
||||
We could permit implicit conversion from arithmetic types to `bool`.
|
||||
|
||||
Advantages:
|
||||
|
||||
- Improves C++ compatibility and familiarity to C++ programmers.
|
||||
|
||||
Disadvantages:
|
||||
|
||||
- Harms type safety by permitting an implicit lossy conversion.
|
||||
- Invites bugs where the wrong overload is selected, where an argument of
|
||||
arithmetic type is passed to a `bool` parameter.
|
||||
- Harms the mental model of `bool` being a choice type rather than an integer
|
||||
type.
|
||||
- Allowing an implicit conversion would permit this kind of conversion
|
||||
everywhere, whereas it is likely only desirable in a select few places, such
|
||||
as where C++ performs a "contextual conversion to `bool`".
|
||||
|
||||
### No conversions
|
||||
|
||||
We could permit no implicit conversions at all, or restrict the set of
|
||||
conversions from those proposed.
|
||||
|
||||
Advantages:
|
||||
|
||||
- Code might be easier to understand, because all conversions would be fully
|
||||
explicit.
|
||||
|
||||
Disadvantages:
|
||||
|
||||
- Code is likely to be harder to read and harder to write due to casts being
|
||||
inserted frequently.
|
||||
- Creates tension for generics, where implicit conversions between
|
||||
type-of-types are a central part of the model.
|
||||
|
||||
### No extensibility
|
||||
|
||||
We could provide only built-in conversions and no user-defined implicit
|
||||
conversions.
|
||||
|
||||
Advantages:
|
||||
|
||||
- Ensures that programmers don't add irresponsible implicit conversions.
|
||||
|
||||
Disadvantages:
|
||||
|
||||
- Creates an artificial distinction between built-in and user-defined types.
|
||||
- Creates problems for interoperation with C++ and migration from C++, because
|
||||
certain forms of user-defined implicit conversion are common in C++ code.
|
||||
- Disallows useful functionality without sufficient justification.
|
||||
|
||||
### Transitivity
|
||||
|
||||
We could apply implicit conversions transitively. If an implicit conversion from
|
||||
`A` to `B` is provided and an implicit conversion from `B` to `C` is provided,
|
||||
we could try to infer an implicit conversion from `A` to `C`.
|
||||
|
||||
This leads to practical problems, as there would be an unbounded search space
|
||||
for intermediate `B` types. For example:
|
||||
|
||||
```
|
||||
impl [T:! Constraint1] A as ImplicitAs(T);
|
||||
impl [T:! Constraint2] T as ImplicitAs(B);
|
||||
let x: A = ...;
|
||||
let y: B = x as B;
|
||||
```
|
||||
|
||||
There is a potentially unbounded space of types to search here (anything that
|
||||
satisfies both `Constraint1` and `Constraint2` at once. Similarly:
|
||||
|
||||
```
|
||||
class X(N: i32, M: i1) {}
|
||||
impl [template N:! i32] X(N, 0) as ImplicitAs(X(N+1, 0));
|
||||
impl [template N:! i32] X(N, 0) as ImplicitAs(X(N+1, 1));
|
||||
impl [template N:! i32] X(N, 1) as ImplicitAs(X(N+1, 1));
|
||||
let z: auto = ({} as X(0, 0)) as X(100, 0);
|
||||
```
|
||||
|
||||
This could lead to a very long implicit conversion sequence (which will
|
||||
presumably need exponential runtime to find).
|
||||
|
||||
We could support partial transitivity, for only unparameterized intermediate
|
||||
types, by ignoring all blanket impls. But that would be arbitrary, and we can
|
||||
provide better results by first matching the overall source and destination
|
||||
types and then asking them what intermediate type we should be converting to,
|
||||
which is supported by this proposal. For example, for `Optional`:
|
||||
|
||||
```
|
||||
impl [T:! Type, U:! ImplicitAs(T)] U as ImplicitAs(Optional(T)) {
|
||||
fn Convert[me: T]() -> Optional(T) { return ...; }
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user