Skip to content

Commit

Permalink
Tuples and tuple indexing (#3646)
Browse files Browse the repository at this point in the history
Add support for extracting elements of a tuple by their numerical index.

Also formally add the well-established basic syntactic and semantic
rules for
tuples, for which we have had leads issues but no proposal, into the
design.
  • Loading branch information
zygoloid authored Jan 31, 2024
1 parent 9e7a17b commit a1655b6
Show file tree
Hide file tree
Showing 6 changed files with 452 additions and 72 deletions.
11 changes: 0 additions & 11 deletions docs/design/expressions/indexing.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
- [Overview](#overview)
- [Details](#details)
- [Examples](#examples)
- [Open questions](#open-questions)
- [Tuple indexing](#tuple-indexing)
- [Alternatives considered](#alternatives-considered)
- [References](#references)

Expand Down Expand Up @@ -133,15 +131,6 @@ class Span(T:! type) {
}
```

## Open questions

### Tuple indexing

It is not clear how tuple indexing will be modeled. When indexing a tuple, the
index value must be a constant, and the type of the expression can depend on
that value, but we don't yet have the tools to express those properties in a
Carbon API.

## Alternatives considered

- [Different subscripting syntaxes](/proposals/p2274.md#different-subscripting-syntaxes)
Expand Down
51 changes: 51 additions & 0 deletions docs/design/expressions/member_access.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
- [Instance binding](#instance-binding)
- [Non-instance members](#non-instance-members)
- [Non-vacuous member access restriction](#non-vacuous-member-access-restriction)
- [Tuple indexing](#tuple-indexing)
- [Precedence and associativity](#precedence-and-associativity)
- [Alternatives considered](#alternatives-considered)
- [References](#references)
Expand Down Expand Up @@ -841,6 +842,56 @@ alias X3 = (i32 as Factory).Make;
alias X4 = i32.((i32 as Factory).Make);
```

## Tuple indexing

A tuple indexing expression is of the form:

- _expression_ `.` _integer-literal_
- _expression_ `->` _integer-literal_

The _expression_ is required to be of tuple type.

Each positional element of the tuple is considered to have a name that is the
corresponding decimal integer: `0`, `1`, and so on. The spelling of the
_integer-literal_ is required to exactly match one of those names, and the
result is the corresponding element of the tuple.

```
// ✅ `a == 42`.
let a: i32 = (41, 42, 43).1;
// ❌ Error: no tuple element named `0x1`.
let b: i32 = (1, 2, 3).0x1;
// ❌ Error: no tuple element named `2`.
let c: i32 = (1, 2).2;
var t: (i32, i32, i32) = (1, 2, 3);
let p: (i32, i32, i32)* = &t;
// ✅ `m == 3`.
let m: i32 = p->2;
```

In a compound member access of the form:

- _expression_ `.` `(` _expression_ `)`
- _expression_ `->` `(` _expression_ `)`

in which the first _expression_ is a tuple and the second _expression_ is of
integer or integer literal type, the second _expression_ is required to be a
non-negative template constant that is less than the number of tuple elements,
and the result is the corresponding positional element of the tuple.

```
// ✅ `d == 43`.
let d: i32 = (41, 42, 43).(1 + 1);
// ✅ `e == 2`.
let template e:! i32 = (1, 2, 3).(0x1);
// ❌ Error: no tuple element with index 4.
let f: i32 = (1, 2).(2 * 2);
// ✅ `n == 3`.
let n: i32 = p->(e);
```

## Precedence and associativity

Member access expressions associate left-to-right:
Expand Down
9 changes: 8 additions & 1 deletion docs/design/lexical_conventions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,11 @@ A _lexical element_ is one of the following:
- a [symbolic token](symbolic_tokens.md)

The sequence of lexical elements is formed by repeatedly removing the longest
initial sequence of characters that forms a valid lexical element.
initial sequence of characters that forms a valid lexical element, with the
following exception:

- When a numeric literal immediately follows a `.` or `->` token, with no
intervening whitespace, a real literal is never formed. Instead, the token
will end no later than the next `.` character. For example, `tuple.1.2` is
five tokens, `tuple` `.` `1` `.` `2`, not three tokens, `tuple` `.` `1.2`.
However, `tuple . 1.2` is lexed as three tokens.
7 changes: 6 additions & 1 deletion docs/design/lexical_conventions/numeric_literals.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,12 @@ or C++ octal literal (other than `0`) is invalid in Carbon.
Real numbers are written as a decimal or hexadecimal integer followed by a
period (`.`) followed by a sequence of one or more decimal or hexadecimal
digits, respectively. A digit is required on each side of the period. `0.` and
`.3` are both invalid.
`.3` are both lexed as two separate tokens: `0.(Util.Abs)()` and `tuple.3` both
treat the period as member or element access, not as a radix point.

To support tuple indexing, a real number literal is never formed immediately
following a `.` token with no intervening whitespace. Instead, the result is an
integer literal.

A real number can be followed by an exponent character, an optional `+` or `-`
(defaulting to `+` if absent), and a character sequence matching the grammar of
Expand Down
151 changes: 92 additions & 59 deletions docs/design/tuples.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,25 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

## Table of contents

- [TODO](#todo)
- [Overview](#overview)
- [Element access](#element-access)
- [Empty tuples](#empty-tuples)
- [Indices as compile-time constants](#indices-as-compile-time-constants)
- [Trailing commas and single-element tuples](#trailing-commas-and-single-element-tuples)
- [Tuple of types and tuple types](#tuple-of-types-and-tuple-types)
- [Operations performed field-wise](#operations-performed-field-wise)
- [Pattern matching](#pattern-matching)
- [Open questions](#open-questions)
- [Tuple slicing](#tuple-slicing)
- [Slicing ranges](#slicing-ranges)
- [Single-value tuples](#single-value-tuples)
- [Function pattern match](#function-pattern-match)
- [Type vs tuple of types](#type-vs-tuple-of-types)
- [Alternatives considered](#alternatives-considered)
- [References](#references)

<!-- tocstop -->

## 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

The primary composite type involves simple aggregation of other types as a tuple
(called a "product type" in formal type theory):
The primary composite type involves simple aggregation of other types as a
tuple, called a "product type" in formal type theory:

```
fn DoubleBoth(x: i32, y: i32) -> (i32, i32) {
Expand All @@ -49,39 +44,55 @@ and second elements are expressions referring to the `i32` type. The only
difference is the type of these expressions. Both are tuples, but one is a tuple
of types.

Element access uses subscript syntax:
## Element access

Element access uses a syntax similar to field access, with an element index
instead of a field name:

```
fn Bar(x: i32, y: i32) -> i32 {
fn Sum(x: i32, y: i32) -> i32 {
var t: (i32, i32) = (x, y);
return t[0] + t[1];
return t.0 + t.1;
}
```

Tuples also support multiple indices and slicing to restructure tuple elements:
A parenthesized template constant expression can also be used to index a tuple:

```
fn Baz(x: i32, y: i32, z: i32) -> (i32, i32) {
var t1: (i32, i32, i32) = (x, y, z);
var t2: (i32, i32, i32) = t1[(2, 1, 0)];
return t2[0 .. 2];
fn Choose(template N:! i32) -> i32 {
return (1, 2, 3).(N % 3);
}
```

This code first reverses the tuple, and then extracts a slice using a half-open
range of indices.

### Empty tuples

`()` is the empty tuple. This is used in other parts of the design, such as
[functions](functions.md).
[functions](functions.md), where a type with a single value is needed.

### Trailing commas and single-element tuples

The final element in a tuple literal may be followed by a trailing comma, such
as `(1, 2,)`. This trailing comma is optional in tuples with two or more
elements, and mandatory in a tuple with a single element: `(x,)` is a one-tuple,
whereas `(x)` is a parenthesized single expression.

### Indices as compile-time constants
### Tuple of types and tuple types

In the example `t1[(2, 1, 0)]`, we will likely want to restrict these indices to
compile-time constants. Without that, run-time indexing would need to suddenly
switch to a variant-style return type to handle heterogeneous tuples. This would
both be surprising and complex for little or no value.
A tuple of types can be used in contexts where a type is needed. This is made
possible by a built-in implicit conversion: a tuple can be implicitly converted
to type `type` if all of its elements can be converted to type `type`, and the
result of the conversion is the corresponding tuple type.

For example, `(i32, i32)` is a value of type `(type, type)`, which is not a type
but can be implicitly converted to a type. `(i32, i32) as type` can be used to
explicitly refer to the corresponding tuple type, which is the type of
expressions such as `(1 as i32, 2 as i32)`. However, this is rarely necessary,
as contexts requiring a type will implicitly convert their operand to a type:

```carbon
// OK, both (i32, i32) values are implicitly converted to `type`.
fn F(x: (i32, i32)) -> (i32, i32);
```

### Operations performed field-wise

Expand All @@ -100,12 +111,39 @@ For binary operations, the two tuples must have the same number of components
and the operation must be defined for the corresponding component types of the
two tuples.

**References:** The rules for assignment, comparison, and implicit conversion
for argument passing were decided in
[question-for-leads issue #710](https://github.com/carbon-language/carbon-lang/issues/710).
### Pattern matching

Tuple values can be matched using a
[tuple pattern](/docs/design/pattern_matching.md#tuple-patterns), which is
written as a tuple of element patterns:

```carbon
let tup: (i32, i32, i32) = (1, 2, 3);
match (tup) {
case (a: i32, 2, var c: i32) => {
c = a;
return c + 1;
}
}
```

## Open questions

### Tuple slicing

Tuples could support multiple indices and slicing to restructure tuple elements:

```
fn Baz(x: i32, y: i32, z: i32) -> (i32, i32) {
var t1: (i32, i32, i32) = (x, y, z);
var t2: (i32, i32, i32) = t1.((2, 1, 0));
return t2.(0 .. 2);
}
```

This code would first reverse the tuple, and then extract a slice using a
half-open range of indices.

### Slicing ranges

The intent of `0 .. 2` is to be syntax for forming a sequence of indices based
Expand All @@ -125,28 +163,23 @@ answer here:
- Do we want to require the `..` to be surrounded by whitespace to
minimize that collision?

### Single-value tuples

This remains an area of active investigation. There are serious problems with
all approaches here. Without the collapse of one-tuples to scalars we need to
distinguish between a parenthesized single expression (`(42)`) and a one-tuple
(in Python or Rust, `(42,)`), and if we distinguish them then we cannot model a
function call as simply a function name followed by a tuple of arguments; one of
`f(0)` and `f(0,)` becomes a special case. With the collapse, we either break
genericity by forbidding `(42)[0]` from working, or it isn't clear what it means
to access a nested tuple's first element from a parenthesized single expression:
`((1, 2))[0]`.

### Function pattern match

There are some interesting corner cases we need to expand on to fully and more
precisely talk about the exact semantic model of function calls and their
pattern match here, especially to handle variadic patterns and forwarding of
tuples as arguments. We are hoping for a purely type system answer here without
needing templates to be directly involved outside the type system as happens in
C++ variadics.

### Type vs tuple of types

Is `(i32, i32)` a type, a tuple of types, or is there even a difference between
the two? Is different syntax needed for these cases?
## Alternatives considered

- [Indexing with square brackets](/proposals/p3646.md#square-bracket-notation)
- [Indexing from the end of a tuple](/proposals/p3646.md#negative-indexing-from-the-end-of-the-tuple)
- [Restrict indexes to decimal integers](/proposals/p3646.md#decimal-indexing-restriction)
- [Alternatives to trailing commas](/proposals/p3646.md#trailing-commas)

## References

- Proposal
[#2188: Pattern matching syntax and semantics](https://github.com/carbon-language/carbon-lang/pull/2188)
- Proposal
[#2360: Types are values of type `type`](https://github.com/carbon-language/carbon-lang/pull/2360)
- Proposal
[#3646: Tuples and tuple indexing](https://github.com/carbon-language/carbon-lang/pull/3646)
- Leads issue
[#710](https://github.com/carbon-language/carbon-lang/issues/710)
established rules for assignment, comparison, and implicit conversion
- Leads issue
[#2191: one-tuples and one-tuple syntax](https://github.com/carbon-language/carbon-lang/issues/2191)
Loading

0 comments on commit a1655b6

Please sign in to comment.