From c74b1752f394b78e62d31c38cca2cbc2e0d55fb1 Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Wed, 8 Sep 2021 13:09:00 -0700 Subject: [PATCH 01/16] Filling out template with PR 820 --- proposals/README.md | 1 + proposals/p0820.md | 62 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 proposals/p0820.md diff --git a/proposals/README.md b/proposals/README.md index 283c2e14a5245..d215ab1ce5c64 100644 --- a/proposals/README.md +++ b/proposals/README.md @@ -69,5 +69,6 @@ request: - [0680 - And, or, not](p0680.md) - [0722 - Nominal classes and methods](p0722.md) - [0731 - Generics details 2: adapters, associated types, parameterized interfaces](p0731.md) +- [0820 - Implicit conversions](p0820.md) diff --git a/proposals/p0820.md b/proposals/p0820.md new file mode 100644 index 0000000000000..554bdaff10b9b --- /dev/null +++ b/proposals/p0820.md @@ -0,0 +1,62 @@ +# Implicit conversions + + + +[Pull request](https://github.com/carbon-language/carbon-lang/pull/820) + + + +## Table of contents + +- [Problem](#problem) +- [Background](#background) +- [Proposal](#proposal) +- [Details](#details) +- [Rationale based on Carbon's goals](#rationale-based-on-carbons-goals) +- [Alternatives considered](#alternatives-considered) + + + +## Problem + +TODO: What problem are you trying to solve? How important is that problem? Who +is impacted by it? + +## Background + +TODO: Is there any background that readers should consider to fully understand +this problem and your approach to solving it? + +## Proposal + +TODO: Briefly and at a high level, how do you propose to solve the problem? Why +will that in fact solve it? + +## Details + +TODO: Fully explain the details of the proposed solution. + +## Rationale based on Carbon's goals + +TODO: How does this proposal effectively advance Carbon's goals? Rather than +re-stating the full motivation, this should connect that motivation back to +Carbon's stated goals for the project or language. This may evolve during +review. Use links to appropriate goals, for example: + +- [Community and culture](/docs/project/goals.md#community-and-culture) +- [Language tools and ecosystem](/docs/project/goals.md#language-tools-and-ecosystem) +- [Performance-critical software](/docs/project/goals.md#performance-critical-software) +- [Software and language evolution](/docs/project/goals.md#software-and-language-evolution) +- [Code that is easy to read, understand, and write](/docs/project/goals.md#code-that-is-easy-to-read-understand-and-write) +- [Practical safety and testing mechanisms](/docs/project/goals.md#practical-safety-and-testing-mechanisms) +- [Fast and scalable development](/docs/project/goals.md#fast-and-scalable-development) +- [Modern OS platforms, hardware architectures, and environments](/docs/project/goals.md#modern-os-platforms-hardware-architectures-and-environments) +- [Interoperability with and migration from existing C++ code](/docs/project/goals.md#interoperability-with-and-migration-from-existing-c-code) + +## Alternatives considered + +TODO: What alternative solutions have you considered? From e6f848a07a4933aa3874b27092f289979e0f5eed Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Wed, 8 Sep 2021 18:17:39 -0700 Subject: [PATCH 02/16] First draft. --- docs/design/expressions/README.md | 52 ++++ .../expressions/implicit_conversions.md | 120 +++++++++ docs/design/generics/details.md | 65 ++--- docs/design/generics/terminology.md | 11 +- proposals/p0820.md | 230 ++++++++++++++++-- 5 files changed, 421 insertions(+), 57 deletions(-) create mode 100644 docs/design/expressions/README.md create mode 100644 docs/design/expressions/implicit_conversions.md diff --git a/docs/design/expressions/README.md b/docs/design/expressions/README.md new file mode 100644 index 0000000000000..2a1476d460a08 --- /dev/null +++ b/docs/design/expressions/README.md @@ -0,0 +1,52 @@ +# Expressions + + + + + +## Table of contents + +- [TODO](#todo) +- [Overview](#overview) +- [Implicit conversions](#implicit-conversions) + + + +## TODO + +This is a skeletal design, added to support [the overview](../README.md). It +should not be treated as accepted by the core team; rather, it is a placeholder +until we have more time to examine this detail. Please feel welcome to rewrite +and update as appropriate. + +## 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); +} +``` diff --git a/docs/design/expressions/implicit_conversions.md b/docs/design/expressions/implicit_conversions.md new file mode 100644 index 0000000000000..c3f11dffdb011 --- /dev/null +++ b/docs/design/expressions/implicit_conversions.md @@ -0,0 +1,120 @@ +# Implicit conversions + + + + + +## Table of contents + +- [Overview](#overview) +- [Built-in types](#built-in-types) + - [Data types](#data-types) + - [Type-of-types](#type-of-types) +- [Semantics](#semantics) +- [Extensibility](#extensibility) +- [Alternatives considered](#alternatives-considered) +- [References](#references) + + + +## 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_: every possible value for the source expression + converts to a distinct representation in the target type. +- The conversion is _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. + +## 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`: + - `i11` or `u11` (or smaller) -> `f16` + - `i24` or `u24` (or smaller) -> `f32` + - `i53` or `u53` (or smaller) -> `f64` + - `i64` or `u64` (or smaller) -> `f80` (x86 only) + - `i113` or `u113` (or smaller) -> `f128` + - `i237` or `u237` (or smaller) -> `f256` + +Further, a constant expression of type `iN`, `uN`, or `fN` can be converted to +any other type `iM`, `uM`, or `fM` in which that value can be exactly +represented. + +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. + +The above conversions are precisely those that C++ considers non-narrowing, +except that 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`. + +The following pointer conversion is available: + +- `T*` -> `U*` if `T` is a subtype of `U` + +### 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` to type `T`, when permitted, always +has the same meaning as the explicit cast expression `E as T`. + +**Note:** The explicit cast expression syntax has not yet been decided. The use +of `E as T` here is provisional. + +## Extensibility + +Implicit conversions can be defined for user-defined types such as +[classes](#../classes.md) by implementing the `ImplicitAs` interface: + +``` +interface As(Type:! Dest) { + fn Convert[me: Self]() -> Dest; +} +interface ImplicitAs(Type:! Dest) extends As(Dest) {} +``` + +When attempting to implicitly convert an expression `x` of type `T` to type `U`, +the expression is rewritten to `(x as (T as ImplicitAs(U))).Convert()`. + +## 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) + +## References + +- [Implicit conversions in C++](https://en.cppreference.com/w/cpp/language/implicit_conversion) +- Proposal + [#820: implicit cnoversions](https://github.com/carbon-language/carbon-lang/pull/820). diff --git a/docs/design/generics/details.md b/docs/design/generics/details.md index 3444fd63faad4..ccd829f9a4f15 100644 --- a/docs/design/generics/details.md +++ b/docs/design/generics/details.md @@ -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. ``` diff --git a/docs/design/generics/terminology.md b/docs/design/generics/terminology.md index f040018db7736..6979cfad8a66d 100644 --- a/docs/design/generics/terminology.md +++ b/docs/design/generics/terminology.md @@ -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 diff --git a/proposals/p0820.md b/proposals/p0820.md index 554bdaff10b9b..c616630309546 100644 --- a/proposals/p0820.md +++ b/proposals/p0820.md @@ -15,48 +15,234 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - [Problem](#problem) - [Background](#background) - [Proposal](#proposal) -- [Details](#details) - [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) ## Problem -TODO: What problem are you trying to solve? How important is that problem? Who -is impacted by it? +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 -TODO: Is there any background that readers should consider to fully understand -this problem and your approach to solving it? +C++ permits many kinds of implicit conversion, some of which are generally +considered good, and others are sometimes harmful. For example: -## Proposal +- `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. -TODO: Briefly and at a high level, how do you propose to solve the problem? Why -will that in fact solve it? +See also +[implicit conversions in C++](https://en.cppreference.com/w/cpp/language/implicit_conversion). -## Details +## Proposal -TODO: Fully explain the details of the proposed solution. +See changes to design. ## Rationale based on Carbon's goals -TODO: How does this proposal effectively advance Carbon's goals? Rather than -re-stating the full motivation, this should connect that motivation back to -Carbon's stated goals for the project or language. This may evolve during -review. Use links to appropriate goals, for example: - -- [Community and culture](/docs/project/goals.md#community-and-culture) -- [Language tools and ecosystem](/docs/project/goals.md#language-tools-and-ecosystem) -- [Performance-critical software](/docs/project/goals.md#performance-critical-software) - [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) -- [Practical safety and testing mechanisms](/docs/project/goals.md#practical-safety-and-testing-mechanisms) -- [Fast and scalable development](/docs/project/goals.md#fast-and-scalable-development) -- [Modern OS platforms, hardware architectures, and environments](/docs/project/goals.md#modern-os-platforms-hardware-architectures-and-environments) + - 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 -TODO: What alternative solutions have you 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. From 8e0b017841630add2d166efded1591117ea0a7cf Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Thu, 9 Sep 2021 19:03:30 -0700 Subject: [PATCH 03/16] Improve wording; permit implicit conversion between facet types. --- .../expressions/implicit_conversions.md | 102 +++++++++++++++--- 1 file changed, 89 insertions(+), 13 deletions(-) diff --git a/docs/design/expressions/implicit_conversions.md b/docs/design/expressions/implicit_conversions.md index c3f11dffdb011..55ddcb99f3a9f 100644 --- a/docs/design/expressions/implicit_conversions.md +++ b/docs/design/expressions/implicit_conversions.md @@ -13,6 +13,8 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - [Overview](#overview) - [Built-in types](#built-in-types) - [Data types](#data-types) + - [Equivalent types](#equivalent-types) + - [Pointer conversions](#pointer-conversions) - [Type-of-types](#type-of-types) - [Semantics](#semantics) - [Extensibility](#extensibility) @@ -58,24 +60,88 @@ The following implicit numeric conversions are available: - `i24` or `u24` (or smaller) -> `f32` - `i53` or `u53` (or smaller) -> `f64` - `i64` or `u64` (or smaller) -> `f80` (x86 only) - - `i113` or `u113` (or smaller) -> `f128` - - `i237` or `u237` (or smaller) -> `f256` - -Further, a constant expression of type `iN`, `uN`, or `fN` can be converted to -any other type `iM`, `uM`, or `fM` in which that value can be exactly -represented. + - `i113` or `u113` (or smaller) -> `f128` (if available) + - `i237` 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 precisely those that C++ considers non-narrowing, -except that 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`. +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 + [#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 represent the same set of values and can be +used interchangeably, implicitly. This refines the notion of types being +compatible, where the representation is the same but an explicit cast may be +required to view a value of one type with a compatible but non-equivalent type. + +`T` is equivalent to `U` if: + +- `T` is `A*`, `U` is `B*`, and `A` is equivalent to `B`, or +- `T` is the facet type `U as SomeInterface`, or +- `U` is the facet type `T as SomeInterface`, 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. + +### Pointer conversions The following pointer conversion is available: -- `T*` -> `U*` if `T` is a subtype of `U` +- `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 a class derived from `U`. + +**Note:** More type subtyping rules are expected to be added over time. + +Note that `T*` is not 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 should treat `const T*` as a +subtype of `const U*` if `T` is a subtype of `U`, and should treat `T` as a +subtype of `const T`. ### Type-of-types @@ -86,11 +152,14 @@ of `TT2`. ## Semantics -An implicit conversion of an expression `E` to type `T`, when permitted, always -has the same meaning as the explicit cast expression `E as T`. +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` here is provisional. +of `E as T` in this document is provisional. ## Extensibility @@ -107,6 +176,13 @@ interface ImplicitAs(Type:! Dest) extends As(Dest) {} When attempting to implicitly convert an expression `x` of type `T` to type `U`, the expression is rewritten to `(x as (T as ImplicitAs(U))).Convert()`. +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) From d9f5ebd8139a38d7e0d86ccb463619bc16db48f2 Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Thu, 9 Sep 2021 19:04:38 -0700 Subject: [PATCH 04/16] Remove unnecessary TODO. --- docs/design/expressions/README.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/docs/design/expressions/README.md b/docs/design/expressions/README.md index 2a1476d460a08..2eb454590df9b 100644 --- a/docs/design/expressions/README.md +++ b/docs/design/expressions/README.md @@ -10,19 +10,11 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception ## Table of contents -- [TODO](#todo) - [Overview](#overview) - [Implicit conversions](#implicit-conversions) -## TODO - -This is a skeletal design, added to support [the overview](../README.md). It -should not be treated as accepted by the core team; rather, it is a placeholder -until we have more time to examine this detail. Please feel welcome to rewrite -and update as appropriate. - ## Overview Expressions are the portions of Carbon syntax that produce values. Because types From 62f02af1946cfdc1c8f69670a873da81b8bec323 Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Fri, 10 Sep 2021 18:37:54 -0700 Subject: [PATCH 05/16] Apply suggestions from code review Co-authored-by: josh11b --- docs/design/expressions/implicit_conversions.md | 10 +++++----- proposals/p0820.md | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/design/expressions/implicit_conversions.md b/docs/design/expressions/implicit_conversions.md index 55ddcb99f3a9f..0cf4c1bfb7d1f 100644 --- a/docs/design/expressions/implicit_conversions.md +++ b/docs/design/expressions/implicit_conversions.md @@ -87,7 +87,7 @@ except: **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 + have singleton types; see issue [#508](https://github.com/carbon-language/carbon-lang/issues/508). ### Equivalent types @@ -164,13 +164,13 @@ 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: +[classes](../classes.md) by implementing the `ImplicitAs` interface: ``` -interface As(Type:! Dest) { +interface As(Dest:! Type) { fn Convert[me: Self]() -> Dest; } -interface ImplicitAs(Type:! Dest) extends As(Dest) {} +interface ImplicitAs(Dest:! Type) extends As(Dest) {} ``` When attempting to implicitly convert an expression `x` of type `T` to type `U`, @@ -193,4 +193,4 @@ types. - [Implicit conversions in C++](https://en.cppreference.com/w/cpp/language/implicit_conversion) - Proposal - [#820: implicit cnoversions](https://github.com/carbon-language/carbon-lang/pull/820). + [#820: implicit conversions](https://github.com/carbon-language/carbon-lang/pull/820). diff --git a/proposals/p0820.md b/proposals/p0820.md index c616630309546..fe1f573a57e48 100644 --- a/proposals/p0820.md +++ b/proposals/p0820.md @@ -92,21 +92,21 @@ deviation and a rationale. 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 +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.) +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 +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.) +from a function value to such a type-erased generic value. #### Qualification conversions From c4f5162ba093e471b7e68cbb7ee098b4a98f458e Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Fri, 10 Sep 2021 18:39:07 -0700 Subject: [PATCH 06/16] We can fit a one-bit-wider signed int into a floating-point type than we can for an unsigned type. We get a sign bit for free in each floating-point type, which covers all but the lowest value in iN, and the lowest value is always exactly representable because it's -2^M for a sufficiently small M. --- docs/design/expressions/implicit_conversions.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/design/expressions/implicit_conversions.md b/docs/design/expressions/implicit_conversions.md index 0cf4c1bfb7d1f..6be65b28d69fc 100644 --- a/docs/design/expressions/implicit_conversions.md +++ b/docs/design/expressions/implicit_conversions.md @@ -56,12 +56,12 @@ The following implicit numeric conversions are available: - `fN` -> `fM` if `M` > `N` - `iN` or `uN` -> `fM` if every value of type `iN` or `uN` can be represeted in `fM`: - - `i11` or `u11` (or smaller) -> `f16` - - `i24` or `u24` (or smaller) -> `f32` - - `i53` or `u53` (or smaller) -> `f64` - - `i64` or `u64` (or smaller) -> `f80` (x86 only) - - `i113` or `u113` (or smaller) -> `f128` (if available) - - `i237` or `u237` (or smaller) -> `f256` (if available) + - `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. From 3b1d9319149932bb103ff4146a855b5568bb7668 Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Fri, 10 Sep 2021 18:42:12 -0700 Subject: [PATCH 07/16] Add link to definition of 'compatible type'. --- docs/design/expressions/implicit_conversions.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/design/expressions/implicit_conversions.md b/docs/design/expressions/implicit_conversions.md index 6be65b28d69fc..45f138fbae5ef 100644 --- a/docs/design/expressions/implicit_conversions.md +++ b/docs/design/expressions/implicit_conversions.md @@ -98,8 +98,9 @@ The following conversion is available: Two types are equivalent if they can represent the same set of values and can be used interchangeably, implicitly. This refines the notion of types being -compatible, where the representation is the same but an explicit cast may be -required to view a value of one type with a compatible but non-equivalent type. +[compatible](../generics/terminology.md#compatible-types), where the +representation is the same but an explicit cast may be required to view a value +of one type with a compatible but non-equivalent type. `T` is equivalent to `U` if: From 5a1b48243649a8ee534acb5f80af7851863025f3 Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Fri, 10 Sep 2021 18:42:57 -0700 Subject: [PATCH 08/16] Update docs/design/expressions/implicit_conversions.md Co-authored-by: Chandler Carruth --- docs/design/expressions/implicit_conversions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/design/expressions/implicit_conversions.md b/docs/design/expressions/implicit_conversions.md index 45f138fbae5ef..c7e1309399fd3 100644 --- a/docs/design/expressions/implicit_conversions.md +++ b/docs/design/expressions/implicit_conversions.md @@ -73,7 +73,7 @@ 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 precisely those that C++ considers non-narrowing, +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 From 3d9ec8b0ef0c1a2f5f4f3867cc1a8bc3216948f8 Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Wed, 15 Sep 2021 14:25:14 -0700 Subject: [PATCH 09/16] Attempt to clarify terminology. Add more examples. --- .../expressions/implicit_conversions.md | 59 ++++++++++++++++--- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/docs/design/expressions/implicit_conversions.md b/docs/design/expressions/implicit_conversions.md index c7e1309399fd3..7436fb6263534 100644 --- a/docs/design/expressions/implicit_conversions.md +++ b/docs/design/expressions/implicit_conversions.md @@ -15,6 +15,7 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - [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) @@ -96,21 +97,32 @@ The following conversion is available: - `T` -> `U` if `T` is equivalent to `U` -Two types are equivalent if they can represent the same set of values and can be -used interchangeably, implicitly. This refines the notion of types being -[compatible](../generics/terminology.md#compatible-types), where the -representation is the same but an explicit cast may be required to view a value -of one type with a compatible but non-equivalent type. +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 `A*`, `U` is `B*`, and `A` is equivalent to `B`, 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: @@ -124,7 +136,7 @@ The following pointer conversion is available: **Note:** More type subtyping rules are expected to be added over time. -Note that `T*` is not a subtype of `U*` even if `T` is a subtype of `U`. For +`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*`: @@ -140,10 +152,39 @@ var r: Base** = &p; *r = q; ``` -**Note:** If we add `const` qualification, we should treat `const T*` as a -subtype of `const U*` if `T` is a subtype of `U`, and should treat `T` as a +**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*` + +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` From 90a5643298d4cb4baa4d4fed9042bad03f1ab33f Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Wed, 15 Sep 2021 14:57:53 -0700 Subject: [PATCH 10/16] Allow equivalent type conversions on the pointee of a pointer type as part of a derived-to-base pointer conversion. --- docs/design/expressions/implicit_conversions.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/design/expressions/implicit_conversions.md b/docs/design/expressions/implicit_conversions.md index 7436fb6263534..548e576d2cafe 100644 --- a/docs/design/expressions/implicit_conversions.md +++ b/docs/design/expressions/implicit_conversions.md @@ -104,6 +104,7 @@ 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 @@ -132,7 +133,7 @@ The following pointer conversion is available: `T` is a subtype of `U` if: - `T` is equivalent to `U`, as described above, or -- `T` is a class derived from `U`. +- `T` is equivalent to a class derived from a class eqiuvalent to `U`. **Note:** More type subtyping rules are expected to be added over time. @@ -173,6 +174,8 @@ These implicit pointer conversions are permitted: - `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: From 6ecb91aeb6f8bd619cb9bb633c894cb7046a6122 Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Wed, 15 Sep 2021 17:06:00 -0700 Subject: [PATCH 11/16] Responses to review comments. --- docs/design/expressions/implicit_conversions.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/design/expressions/implicit_conversions.md b/docs/design/expressions/implicit_conversions.md index 548e576d2cafe..6db544ffa9c86 100644 --- a/docs/design/expressions/implicit_conversions.md +++ b/docs/design/expressions/implicit_conversions.md @@ -33,7 +33,7 @@ possible. For [built-in types](#built-in-types), implicit conversions are permitted when: - The conversion is _lossless_: every possible value for the source expression - converts to a distinct representation in the target type. + converts to a distinct value in the target type. - The conversion is _semantics-preserving_: corresponding values in the source and destination type have the same abstract meaning. @@ -218,8 +218,13 @@ interface As(Dest:! Type) { interface ImplicitAs(Dest:! Type) extends As(Dest) {} ``` -When attempting to implicitly convert an expression `x` of type `T` to type `U`, -the expression is rewritten to `(x as (T as ImplicitAs(U))).Convert()`. +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 From bd6d034e3a18f0c249588c5cd02c7c709a03ed4d Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Mon, 20 Sep 2021 12:54:12 -0700 Subject: [PATCH 12/16] Fix typo. Co-authored-by: josh11b --- docs/design/expressions/implicit_conversions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/design/expressions/implicit_conversions.md b/docs/design/expressions/implicit_conversions.md index 6db544ffa9c86..0880d01c2bc79 100644 --- a/docs/design/expressions/implicit_conversions.md +++ b/docs/design/expressions/implicit_conversions.md @@ -133,7 +133,7 @@ The following pointer conversion is available: `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 eqiuvalent to `U`. +- `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. From 033a02a72a113c12e837928fcde131e76a91ee05 Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Mon, 20 Sep 2021 13:51:53 -0700 Subject: [PATCH 13/16] Add more detail and examples for lossless and semantics-preserving. --- .../expressions/implicit_conversions.md | 58 +++++++++++++++++-- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/docs/design/expressions/implicit_conversions.md b/docs/design/expressions/implicit_conversions.md index 0880d01c2bc79..31a07d78907cf 100644 --- a/docs/design/expressions/implicit_conversions.md +++ b/docs/design/expressions/implicit_conversions.md @@ -11,6 +11,10 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception ## 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) @@ -32,10 +36,11 @@ possible. For [built-in types](#built-in-types), implicit conversions are permitted when: -- The conversion is _lossless_: every possible value for the source expression - converts to a distinct value in the target type. -- The conversion is _semantics-preserving_: corresponding values in the source - and destination type have the same abstract meaning. +- 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 @@ -46,6 +51,51 @@ 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. A conversion in the opposite direction should be possible, +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 invariants of the source +type. + +### 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 From 1044c5b4461aab3e9b6577018ce2d8db6df5f9d7 Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Mon, 20 Sep 2021 13:58:58 -0700 Subject: [PATCH 14/16] Add alternative considered for applying implicit conversions transitively. --- .../expressions/implicit_conversions.md | 1 + proposals/p0820.md | 43 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/docs/design/expressions/implicit_conversions.md b/docs/design/expressions/implicit_conversions.md index 31a07d78907cf..edc0d3145ad64 100644 --- a/docs/design/expressions/implicit_conversions.md +++ b/docs/design/expressions/implicit_conversions.md @@ -288,6 +288,7 @@ types. - [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 diff --git a/proposals/p0820.md b/proposals/p0820.md index fe1f573a57e48..a8c80275c9ff0 100644 --- a/proposals/p0820.md +++ b/proposals/p0820.md @@ -31,6 +31,7 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - [Boolean conversions](#boolean-conversions) - [No conversions](#no-conversions) - [No extensibility](#no-extensibility) + - [Transitivity](#transitivity) @@ -246,3 +247,45 @@ Disadvantages: - 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 ...; } +} +``` From 111bfa8dc3a12986d525bfdf776d021cb65b8fc5 Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Mon, 20 Sep 2021 17:56:11 -0700 Subject: [PATCH 15/16] Apply suggestions from code review Co-authored-by: Geoff Romer --- docs/design/expressions/implicit_conversions.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/design/expressions/implicit_conversions.md b/docs/design/expressions/implicit_conversions.md index edc0d3145ad64..e55be61eceae8 100644 --- a/docs/design/expressions/implicit_conversions.md +++ b/docs/design/expressions/implicit_conversions.md @@ -57,13 +57,13 @@ rules. We expect implicit conversion to never lose information: if two values are distinguishable before the conversion, they should generally be distinguishable -after the conversion. A conversion in the opposite direction should be possible, -but such a conversion is not expected to be provided in general, and might be -computationally expensive. +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 invariants of the source -type. +type, implicit conversions do not necessarily preserve static information about +the source value. ### Semantics-preserving From 6100230fb4992c7a08ed1b164eb060c73fdfae89 Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Tue, 21 Sep 2021 15:13:42 -0700 Subject: [PATCH 16/16] Reflow for pre-commit. --- docs/design/expressions/implicit_conversions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/design/expressions/implicit_conversions.md b/docs/design/expressions/implicit_conversions.md index e55be61eceae8..3d003ae8fd9b2 100644 --- a/docs/design/expressions/implicit_conversions.md +++ b/docs/design/expressions/implicit_conversions.md @@ -58,8 +58,8 @@ rules. 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. +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