Skip to content

Commit

Permalink
as expressions (#845)
Browse files Browse the repository at this point in the history
This proposal provides an `as` expression for casting. This supports implicit conversions plus some safe and unsurprising conversions that we do not support implicitly:

* lossy but fully defined conversions to floating-point types
* conversion from `bool` to integer types
* conversion between adaptors and their adapted type, and more generally between compatible types

This facility can be extended by implementing the `As(TargetType)` interface for a type.

Co-authored-by: Geoff Romer <[email protected]>
Co-authored-by: josh11b <[email protected]>
  • Loading branch information
3 people authored Oct 29, 2021
1 parent d176fac commit b49584d
Show file tree
Hide file tree
Showing 6 changed files with 600 additions and 21 deletions.
7 changes: 5 additions & 2 deletions docs/design/expressions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
## Table of contents

- [Overview](#overview)
- [Implicit conversions](#implicit-conversions)
- [Conversions and casts](#conversions-and-casts)

<!-- tocstop -->

Expand All @@ -29,12 +29,15 @@ fn Foo(a: i32*) -> i32 {
Here, the parameter type `i32*`, the return type `i32`, and the operand `*a` of
the `return` statement are all expressions.

## Implicit conversions
## Conversions and casts

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.

Expressions can also be converted to a specific type using an
[`as` expression](as_expressions.md).

```
fn Bar(n: i32);
fn Baz(n: i64) {
Expand Down
178 changes: 178 additions & 0 deletions docs/design/expressions/as_expressions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# `as` 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)
- [Precedence and associativity](#precedence-and-associativity)
- [Built-in types](#built-in-types)
- [Data types](#data-types)
- [Compatible types](#compatible-types)
- [Extensibility](#extensibility)
- [Alternatives considered](#alternatives-considered)
- [References](#references)

<!-- tocstop -->

## Overview

An expression of one type can be explicitly cast to another type by using an
`as` expression:

```
var n: i32 = Get();
var f: f32 = n as f32;
```

An `as` expression can be used to perform any implicit conversion, either when
the context does not imply a destination type or when it is valuable to a reader
of the code to make the conversion explicit. In addition, `as` expressions can
perform safe conversions that nonetheless should not be performed implicitly,
such as lossy conversions or conversions that lose capabilities or change the
way a type would be interpreted.

As guidelines, an `as` conversion should be permitted when:

- The conversion is _complete_: it produces a well-defined output value for
each input value.
- The conversion is _unsurprising_: the resulting value is the expected value
in the destination type.

For example:

- A conversion from `fM` to `iN` is not complete, because it is not defined
for input values that are out of the range of the destination type, such as
infinities or, if `N` is too small, large finite values.
- A conversion from `iM` to `iN`, where `N` < `M`, is either not complete or
not unsurprising, because there is more than one possible expected behavior
for an input value that is not within the destination type, and those
behaviors are not substantially the same -- we could perform two's
complement wrapping, saturate, or produce undefined behavior analogous to
arithmetic overflow.
- A conversion from `iM` to `fN` can be unsurprising, because even though
there may be a choice of which way to round, the possible values are
substantially the same.

It is possible for user-defined types to [extend](#extensibility) the set of
valid explicit casts that can be performed by `as`. Such extensions are expected
to follow these guidelines.

## Precedence and associativity

`as` expressions are non-associative.

```
var b: bool = true;
// OK
var n: i32 = (b as i1) as i32;
var m: auto = b as (bool as Hashable);
// Error, ambiguous
var m: auto = b as T as U;
```

The `as` operator has lower precedence than operators that visually bind
tightly:

- prefix symbolic operators
- dereference (`*a`)
- negation (`-a`)
- complement (`~a`)
- postfix symbolic operators
- pointer type formation (`T*`),
- function call (`a(...)`),
- array indexing (`a[...]`), and
- member access (`a.m`).

The `as` operator has higher precedence than assignment and comparison. It is
unordered with respect to binary arithmetic, bitwise operators, and unary `not`.

```
// OK
var x: i32* as Comparable;
// OK, `x as (U*)` not `(x as U)*`.
var y: auto = x as U*;
var a: i32;
var b: i32;
// OK, `(a as i64) < ((*x) as i64)`.
if (a as i64 < *x as i64) {}
// Ambiguous: `(a + b) as i64` or `a + (b as i64)`?
var c: i32 = a + b as i64;
// Ambiguous: `(a as i64) + b` or `a as (i64 + b)`?
var d: i32 = a as i64 + b;
// OK, `(-a) as f64`, not `-(a as f64)`.
// Unfortunately, the former is undefined if `a` is `i32.MinValue()`;
// the latter is not.
var u: f64 = -a as f64;
// OK, `i32 as (GetType())`, not `(i32 as GetType)()`.
var e: i32 as GetType();
```

## Built-in types

### Data types

In addition to the [implicit conversions](implicit_conversions.md#data-types),
the following numeric conversions are supported by `as`:

- `iN`, `uN`, or `fN` -> `fM`, for any `N` and `M`. Values that cannot be
exactly represented are suitably rounded to one of the two nearest
representable values. Very large finite values may be rounded to an
infinity. NaN values are converted to NaN values.

- `bool` -> `iN` or `uN`. `false` converts to `0` and `true` converts to `1`
(or to `-1` for `i1`).

Conversions from numeric types to `bool` are not supported with `as`; instead of
using `as bool`, such conversions can be performed with `!= 0`.

Lossy conversions between `iN` or `uN` and `iM` or `uM` are not supported with
`as`, and similarly conversions from `fN` to `iM` are not supported.

**Future work:** Add mechanisms to perform these conversions.

### Compatible types

The following conversion is supported by `as`:

- `T` -> `U` if `T` is
[compatible](../generics/terminology.md#compatible-types) with `U`.

**Future work:** We may need a mechanism to restrict which conversions between
adapters are permitted and which code can perform them. Some of the conversions
permitted by this rule may only be allowed in certain contexts.

## Extensibility

Explicit casts can be defined for user-defined types such as
[classes](../classes.md) by implementing the `As` interface:

```
interface As(Dest:! Type) {
fn Convert[me: Self]() -> Dest;
}
```

The expression `x as U` is rewritten to `x.(As(U).Convert)()`.

## Alternatives considered

- [Do not distinguish between safe and unsafe casts](/docs/proposals/p0845.md#merge-as-and-assume_as)
- [Do not distinguish between `as` and implicit conversions](/docs/proposals/p0845.md#as-only-performs-implicit-conversions)
- [Allow `iN as bool`](/docs/proposals/p0845.md#integer-to-bool-conversions)
- [Disallow `bool as iN`](/docs/proposals/p0845.md#bool-to-integer-conversions)

## References

- [Implicit conversions in C++](https://en.cppreference.com/w/cpp/language/implicit_conversion)
- Proposal
[#845: `as` expressions](https://github.com/carbon-language/carbon-lang/pull/845).
32 changes: 14 additions & 18 deletions docs/design/expressions/implicit_conversions.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
- [Pointer conversions](#pointer-conversions)
- [Pointer conversion examples](#pointer-conversion-examples)
- [Type-of-types](#type-of-types)
- [Semantics](#semantics)
- [Consistency with `as`](#consistency-with-as)
- [Extensibility](#extensibility)
- [Alternatives considered](#alternatives-considered)
- [References](#references)
Expand Down Expand Up @@ -245,37 +245,33 @@ 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
## Consistency with `as`

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.
permitted, always has the same meaning as the
[explicit cast expression `E as U`](as_expressions.md). Moreover, because such
an implicit conversion is expected to exactly preserve the value,
`(E as U) as T`, if valid, should be expected to result in the same value as
produced by `E` even if the `as T` cast cannot be performed as an implicit
conversion.

## 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, which
extends
[the `As` interface used to implement `as` expressions](as_expressions.md#extensibility):

```
interface As(Dest:! Type) {
fn Convert[me: Self]() -> Dest;
interface ImplicitAs(Dest:! Type) extends As(Dest) {
// Inherited from As(Dest):
// 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
Expand Down
2 changes: 1 addition & 1 deletion docs/design/generics/details.md
Original file line number Diff line number Diff line change
Expand Up @@ -1631,7 +1631,7 @@ 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
converted to `Song`, which implements interface `Printable`, and
`PlayableSong as Hashable` may be implicilty converted to `PlayableSong`, which
`PlayableSong as Hashable` may be implicitly converted to `PlayableSong`, which
implements interface `Media`. This means that it is safe to convert between
`HashMap(Song, i32)` and `HashMap(PlayableSong, i32)` (though maybe only with an
explicit cast), since the implementation of all the methods will use the same
Expand Down
1 change: 1 addition & 0 deletions docs/design/lexical_conventions/words.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ The following words are interpreted as keywords:
- `alias`
- `and`
- `api`
- `as`
- `auto`
- `base`
- `break`
Expand Down
Loading

0 comments on commit b49584d

Please sign in to comment.