mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-09-24 19:40:10 +01:00
Assignment statements (#2511)
Assignment is permitted only as a complete statement, not as a subexpression. Assignment and compound assignment syntax follow C++ in all other respects. Pre-increment is provided. Post-increment is not. Uses of all of these operators are translated into calls to interface members. Co-authored-by: Jon Ross-Perkins <jperkins@google.com> Co-authored-by: Chandler Carruth <chandlerc@gmail.com>
This commit is contained in:
co-authored by
Jon Ross-Perkins
Chandler Carruth
parent
6a59c77715
commit
a89aba2698
+6
-26
@@ -47,7 +47,6 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||
- [Parameters](#parameters)
|
||||
- [`auto` return type](#auto-return-type)
|
||||
- [Blocks and statements](#blocks-and-statements)
|
||||
- [Assignment statements](#assignment-statements)
|
||||
- [Control flow](#control-flow)
|
||||
- [`if` and `else`](#if-and-else)
|
||||
- [Loops](#loops)
|
||||
@@ -252,11 +251,13 @@ A [variable declaration](#variable-var-declarations) has three parts:
|
||||
```
|
||||
|
||||
You can modify the value of a variable with an
|
||||
[assignment statement](#assignment-statements):
|
||||
[assignment statement](assignment.md):
|
||||
|
||||
```carbon
|
||||
i = 3;
|
||||
...
|
||||
++i;
|
||||
...
|
||||
i += 2;
|
||||
```
|
||||
|
||||
@@ -1195,9 +1196,9 @@ fn Positive(a: i64) -> auto {
|
||||
A _block_ is a sequence of _statements_. A block defines a
|
||||
[scope](#declarations-definitions-and-scopes) and, like other scopes, is
|
||||
enclosed in curly braces (`{`...`}`). Each statement is terminated by a
|
||||
semicolon or block. [Expressions](#expressions) and
|
||||
[`var`](#variable-var-declarations) and [`let`](#constant-let-declarations) are
|
||||
valid statements.
|
||||
semicolon or block. [Expressions](#expressions), [assignments](assignment.md)
|
||||
and [`var`](#variable-var-declarations) and [`let`](#constant-let-declarations)
|
||||
are valid statements.
|
||||
|
||||
Statements within a block are normally executed in the order they appear in the
|
||||
source code, except when modified by control-flow statements.
|
||||
@@ -1223,25 +1224,6 @@ fn Foo() {
|
||||
> - Proposal
|
||||
> [#162: Basic Syntax](https://github.com/carbon-language/carbon-lang/pull/162)
|
||||
|
||||
### Assignment statements
|
||||
|
||||
Assignment statements mutate the value of the
|
||||
[l-value](#value-categories-and-value-phases) described on the left-hand side of
|
||||
the assignment.
|
||||
|
||||
- Assignment: `x = y;`. `x` is assigned the value of `y`.
|
||||
- Increment and decrement: `++i;`, `--j;`. `i` is set to `i + 1`, `j` is set
|
||||
to `j - 1`.
|
||||
- Compound assignment: `x += y;`, `x -= y;`, `x *= y;`, `x /= y;`, `x &= y;`,
|
||||
`x |= y;`, `x ^= y;`, `x <<= y;`, `x >>= y;`. `x @= y;` is equivalent to
|
||||
`x = x @ y;` for each operator `@`.
|
||||
|
||||
Unlike C++, these assignments are statements, not expressions, and don't return
|
||||
a value.
|
||||
|
||||
> **Note:** The semantics of assignment are provisional. See pending proposal
|
||||
> [#821: Values, variables, pointers, and references](https://github.com/carbon-language/carbon-lang/pull/821).
|
||||
|
||||
### Control flow
|
||||
|
||||
Blocks of statements are generally executed sequentially. Control-flow
|
||||
@@ -3074,8 +3056,6 @@ The interfaces that correspond to each operator are given by:
|
||||
- Indexing:
|
||||
- `x[y]` is rewritten to use the
|
||||
[`IndexWith` or `IndirectIndexWith`](expressions/indexing.md) interface.
|
||||
- **TODO:** [Assignment](#assignment-statements): `x = y`, `++x`, `x += y`,
|
||||
and so on
|
||||
- **TODO:** Dereference: `*p`
|
||||
- **TODO:** [Move](#move): `~x`
|
||||
- **TODO:** Function call: `f(4)`
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
# Assignment
|
||||
|
||||
<!--
|
||||
Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
||||
Exceptions. See /LICENSE for license information.
|
||||
SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||
-->
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Syntax](#syntax)
|
||||
- [Simple assignment semantics](#simple-assignment-semantics)
|
||||
- [Compound assignment semantics](#compound-assignment-semantics)
|
||||
- [Built-in types](#built-in-types)
|
||||
- [Tuples, structs, choice types, and data classes](#tuples-structs-choice-types-and-data-classes)
|
||||
- [Extensibility](#extensibility)
|
||||
- [Simple assignment](#simple-assignment)
|
||||
- [Arithmetic](#arithmetic)
|
||||
- [Bitwise and bit-shift](#bitwise-and-bit-shift)
|
||||
- [Defaults](#defaults)
|
||||
- [Alternatives considered](#alternatives-considered)
|
||||
- [References](#references)
|
||||
|
||||
<!-- tocstop -->
|
||||
|
||||
## Overview
|
||||
|
||||
Values can be assigned to variables using the `=` operator:
|
||||
|
||||
```
|
||||
var a: i32 = 5;
|
||||
a = 6;
|
||||
```
|
||||
|
||||
For each binary [arithmetic](expressions/arithmetic.md) or
|
||||
[bitwise](expressions/bitwise.md) operator `$`, a corresponding compound
|
||||
assignment `$=` is provided that performs the operation in-place:
|
||||
|
||||
```
|
||||
// Same as `a = a + 1;`
|
||||
a += 1;
|
||||
// Same as `a = a << 3;`
|
||||
a <<= 3;
|
||||
```
|
||||
|
||||
In addition, increment and decrement operators are provided:
|
||||
|
||||
```
|
||||
// Same as `a = a + 1;`
|
||||
++a;
|
||||
// Same as `a = a - 1;`
|
||||
--a;
|
||||
```
|
||||
|
||||
These simple assignment, compound assignment, increment, and decrement operators
|
||||
can only be used as complete statements, not as subexpressions of other
|
||||
operators, even when parenthesized:
|
||||
|
||||
```
|
||||
var n: i32;
|
||||
// Error, assignment is not permitted as a subexpression.
|
||||
if (F() and (n = GetValue()) > 5) {
|
||||
}
|
||||
```
|
||||
|
||||
User-defined types can define the meaning of these operations by
|
||||
[implementing an interface](#extensibility) provided as part of the Carbon
|
||||
standard library.
|
||||
|
||||
## Syntax
|
||||
|
||||
The operands of these operators can be any [expression](expressions/README.md).
|
||||
However, the first operand must be modifiable because it is passed to an
|
||||
`[addr self: Self*]` parameter, which disallows most expression forms other
|
||||
than:
|
||||
|
||||
- The name of a `var` binding.
|
||||
- A dereference of a pointer.
|
||||
- Array indexing that produces a modifiable result.
|
||||
- Member access naming a field, where the object is one of these expressions.
|
||||
|
||||
## Simple assignment semantics
|
||||
|
||||
A simple assignment statement is intended to exactly mirror the semantics of
|
||||
initialization. The following two code snippets should have the same meaning if
|
||||
they are both valid:
|
||||
|
||||
```
|
||||
// Declare and initialize.
|
||||
var v: T = init;
|
||||
```
|
||||
|
||||
```
|
||||
// Declare separately from initialization.
|
||||
// Requires that `T` has an unformed state.
|
||||
var v: T;
|
||||
v = init;
|
||||
```
|
||||
|
||||
This equivalence is not enforced, but when an object is in an unformed state,
|
||||
running the assignment function is _optional_, just like running the destructor
|
||||
is. If the assignment function is not run, the object will be directly
|
||||
initialized from the right-hand side instead. The type is still required to
|
||||
implement `AssignWith` for the assignment to be valid.
|
||||
|
||||
```
|
||||
class C { ... }
|
||||
fn F() -> C {
|
||||
returned var c: C = {...};
|
||||
// `&c` here is `&x` for the first call to `F()`.
|
||||
// `&c` here can be `&y` for the second call to `F()`.
|
||||
return var;
|
||||
}
|
||||
fn G() {
|
||||
var x: C = F();
|
||||
var y: C;
|
||||
y = F();
|
||||
}
|
||||
```
|
||||
|
||||
## Compound assignment semantics
|
||||
|
||||
The syntax `a $= b;` is intended to be syntactic sugar for `a = a $ b;`, except
|
||||
as follows:
|
||||
|
||||
- A type might be able to provide a more efficient implementation for the
|
||||
compound assignment form than for the uncombined form.
|
||||
- A type might not be able to, or might not want to, provide the uncombined
|
||||
form at all, for example because creating a new instance requires additional
|
||||
resources that might not be available, such as a context object or an
|
||||
allocator.
|
||||
|
||||
The syntactic sugar is implemented by a [default implementation](#defaults) of
|
||||
`$=` in terms of `$` and `=`.
|
||||
|
||||
In contrast, `++a;` and `--a;` are not simply syntactic sugar for `a = a + 1;`
|
||||
and `a = a - 1;`. Instead, we interpret these operators as meaning "move to the
|
||||
next value" and "move to the previous value". These operations may be available
|
||||
and meaningful in cases where adding an integer is not a desirable operation,
|
||||
such as for an iterator into a linked list, and may not be available in cases
|
||||
where adding an integer is meaningful, such as for a type representing a
|
||||
rational number.
|
||||
|
||||
## Built-in types
|
||||
|
||||
Integers and floating-point types, `bool`, and pointer types support simple
|
||||
assignment with `=`. The right-hand operand is implicitly converted to the type
|
||||
of the left-hand operand, and the converted value replaces the value of that
|
||||
operand.
|
||||
|
||||
Compound assignment `$=` for integer and floating point types is
|
||||
[provided automatically](#defaults) for each supported operator `$`.
|
||||
|
||||
For integer types, `++n;` and `--n;` behave the same as `n += 1;` and `n -= 1;`
|
||||
respectively. For floating-point types, these operators are not provided.
|
||||
|
||||
## Tuples, structs, choice types, and data classes
|
||||
|
||||
_TODO_: Describe the rules for assignment in these cases.
|
||||
|
||||
See leads issue
|
||||
[#686: Operation order in struct/class assignment/initialization](https://github.com/carbon-language/carbon-lang/issues/686)
|
||||
|
||||
## Extensibility
|
||||
|
||||
Assignment operators can be provided for user-defined types by implementing the
|
||||
following families of interfaces. Implementations of these interfaces are
|
||||
provided for built-in types as necessary to give the semantics described above.
|
||||
|
||||
### Simple assignment
|
||||
|
||||
```
|
||||
// Simple `=`.
|
||||
interface AssignWith(U:! type) {
|
||||
fn Op[addr self: Self*](other: U);
|
||||
}
|
||||
constraint Assign { extends AssignWith(Self); }
|
||||
```
|
||||
|
||||
Given `var x: T` and `y: U`:
|
||||
|
||||
- The statement `x = y;` is rewritten to `x.(AssignWith(U).Op)(y);`.
|
||||
|
||||
### Arithmetic
|
||||
|
||||
```
|
||||
// Compound `+=`.
|
||||
interface AddAssignWith(U:! type) {
|
||||
fn Op[addr self: Self*](other: U);
|
||||
}
|
||||
constraint AddAssign { extends AddAssignWith(Self); }
|
||||
```
|
||||
|
||||
```
|
||||
// Compound `-=`.
|
||||
interface SubAssignWith(U:! type) {
|
||||
fn Op[addr self: Self*](other: U);
|
||||
}
|
||||
constraint SubAssign { extends SubAssignWith(Self); }
|
||||
```
|
||||
|
||||
```
|
||||
// Compound `*=`.
|
||||
interface MulAssignWith(U:! type) {
|
||||
fn Op[addr self: Self*](other: U);
|
||||
}
|
||||
constraint MulAssign { extends MulAssignWith(Self); }
|
||||
```
|
||||
|
||||
```
|
||||
// Compound `/=`.
|
||||
interface DivAssignWith(U:! type) {
|
||||
fn Op[addr self: Self*](other: U);
|
||||
}
|
||||
constraint DivAssign { extends DivAssignWith(Self); }
|
||||
```
|
||||
|
||||
```
|
||||
// Compound `%=`.
|
||||
interface ModAssignWith(U:! type) {
|
||||
fn Op[addr self: Self*](other: U);
|
||||
}
|
||||
constraint ModAssign { extends ModAssignWith(Self); }
|
||||
```
|
||||
|
||||
```
|
||||
// Increment `++`.
|
||||
interface Inc { fn Op[addr self: Self*](); }
|
||||
// Decrement `++`.
|
||||
interface Dec { fn Op[addr self: Self*](); }
|
||||
```
|
||||
|
||||
Given `var x: T` and `y: U`:
|
||||
|
||||
- The statement `x += y;` is rewritten to `x.(AddAssignWith(U).Op)(y);`.
|
||||
- The statement `x -= y;` is rewritten to `x.(SubAssignWith(U).Op)(y);`.
|
||||
- The statement `x *= y;` is rewritten to `x.(MulAssignWith(U).Op)(y);`.
|
||||
- The statement `x /= y;` is rewritten to `x.(DivAssignWith(U).Op)(y);`.
|
||||
- The statement `x %= y;` is rewritten to `x.(ModAssignWith(U).Op)(y);`.
|
||||
- The statement `++x;` is rewritten to `x.(Inc.Op)();`.
|
||||
- The statement `--x;` is rewritten to `x.(Dec.Op)();`.
|
||||
|
||||
### Bitwise and bit-shift
|
||||
|
||||
```
|
||||
// Compound `&=`.
|
||||
interface BitAndAssignWith(U:! type) {
|
||||
fn Op[addr self: Self*](other: U);
|
||||
}
|
||||
constraint BitAndAssign { extends BitAndAssignWith(Self); }
|
||||
```
|
||||
|
||||
```
|
||||
// Compound `|=`.
|
||||
interface BitOrAssignWith(U:! type) {
|
||||
fn Op[addr self: Self*](other: U);
|
||||
}
|
||||
constraint BitOrAssign { extends BitOrAssignWith(Self); }
|
||||
```
|
||||
|
||||
```
|
||||
// Compound `^=`.
|
||||
interface BitXorAssignWith(U:! type) {
|
||||
fn Op[addr self: Self*](other: U);
|
||||
}
|
||||
constraint BitXorAssign { extends BitXorAssignWith(Self); }
|
||||
```
|
||||
|
||||
```
|
||||
// Compound `<<=`.
|
||||
interface LeftShiftAssignWith(U:! type) {
|
||||
fn Op[addr self: Self*](other: U);
|
||||
}
|
||||
constraint LeftShiftAssign { extends LeftShiftAssignWith(Self); }
|
||||
```
|
||||
|
||||
```
|
||||
// Compound `>>=`.
|
||||
interface RightShiftAssignWith(U:! type) {
|
||||
fn Op[addr self: Self*](other: U);
|
||||
}
|
||||
constraint RightShiftAssign { extends RightShiftAssignWith(Self); }
|
||||
```
|
||||
|
||||
Given `var x: T` and `y: U`:
|
||||
|
||||
- The statement `x &= y;` is rewritten to `x.(BitAndAssignWith(U).Op)(y);`.
|
||||
- The statement `x |= y;` is rewritten to `x.(BitOrAssignWith(U).Op)(y);`.
|
||||
- The statement `x ^= y;` is rewritten to `x.(BitXorAssignWith(U).Op)(y);`.
|
||||
- The statement `x <<= y;` is rewritten to
|
||||
`x.(LeftShiftAssignWith(U).Op)(y);`.
|
||||
- The statement `x >>= y;` is rewritten to
|
||||
`x.(RightShiftAssignWith(U).Op)(y)`;.
|
||||
|
||||
Implementations of these interfaces are provided for built-in types as necessary
|
||||
to give the semantics described above.
|
||||
|
||||
### Defaults
|
||||
|
||||
When a type provides both an assignment and a binary operator `$`, so that
|
||||
`a = a $ b;` is valid, Carbon provides a default `$=` implementation so that
|
||||
`a $= b;` is valid and has the same meaning as `a = a $ b;`.
|
||||
|
||||
This defaulting is accomplished by a parameterized implementation of
|
||||
`OpAssignWith(U)` defined in terms of `AssignWith` and `OpWith`:
|
||||
|
||||
```
|
||||
impl forall [U:! type, T:! OpWith(U) where .Self is AssignWith(.Self.Result)]
|
||||
T as OpAssignWith(U) {
|
||||
fn Op[addr self: Self*](other: U) {
|
||||
// Here, `$` is the operator described by `OpWith`.
|
||||
*self = *self $ other;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If a more efficient form of compound assignment is possible for a type, a more
|
||||
specific `impl` can be provided:
|
||||
|
||||
```
|
||||
impl like MyString as AddWith(like MyString) {
|
||||
// Allocate new memory and perform addition.
|
||||
}
|
||||
|
||||
impl MyString as AddAssignWith(like MyString) {
|
||||
// Reuse existing storage where possible.
|
||||
}
|
||||
```
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- [Allow assignment as a subexpression](/proposals/p2511.md#allow-assignment-as-a-subexpression)
|
||||
- [Allow chained assignment](/proposals/p2511.md#allow-chained-assignment)
|
||||
- [Do not provide increment and decrement](/proposals/p2511.md#do-not-provide-increment-and-decrement)
|
||||
- [Treat increment as syntactic sugar for adding `1`](/proposals/p2511.md#treat-increment-as-syntactic-sugar-for-adding-1)
|
||||
- [Define `$` in terms of `$=`](/proposals/p2511.md#define--in-terms-of-)
|
||||
- [Do not allow overloading the behavior of `=`](/proposals/p2511.md#do-not-allow-overloading-the-behavior-of-)
|
||||
- [Treat the left hand side of `=` as a pattern](/proposals/p2511.md#treat-the-left-hand-side-of--as-a-pattern)
|
||||
- [Different names for interfaces](/proposals/p2511.md#different-names-for-interfaces)
|
||||
|
||||
## References
|
||||
|
||||
- Leads issue
|
||||
[#451: Do we want variable-arity operators?](https://github.com/carbon-language/carbon-lang/issues/451)
|
||||
- Proposal
|
||||
[#257: Initialization of memory and variables](https://github.com/carbon-language/carbon-lang/pull/257)
|
||||
- Proposal
|
||||
[#1083: Arithmetic](https://github.com/carbon-language/carbon-lang/pull/1083)
|
||||
- Proposal
|
||||
[#1178: Rework operator interfaces](https://github.com/carbon-language/carbon-lang/pull/1178)
|
||||
- Proposal
|
||||
[#1191: Bitwise and shift operators](https://github.com/carbon-language/carbon-lang/pull/1191)
|
||||
- Proposal
|
||||
[#2511: Assignment statements](https://github.com/carbon-language/carbon-lang/pull/2511)
|
||||
@@ -660,8 +660,9 @@ analogous to equality comparisons:
|
||||
Implicit conversion from a struct type to a data class type is allowed when the
|
||||
set of field names is the same and implicit conversion is defined between the
|
||||
pairs of member types with the same field names. So calling a function
|
||||
effectively performs an assignment from each of the caller's arguments to the
|
||||
function's parameters, and will be valid when those assignments are all valid.
|
||||
effectively performs an initialization of each of the function's parameters from
|
||||
the caller's arguments, and will be valid when those initializations are all
|
||||
valid.
|
||||
|
||||
A data class has an unformed state if all its members do. Treatment of unformed
|
||||
state follows proposal
|
||||
|
||||
@@ -270,6 +270,10 @@ Most expressions are modeled as operators:
|
||||
| Logical | [`or`](logical_operators.md) | `x or y` | A short-circuiting logical OR: `true` if either operand is `true`. |
|
||||
| Logical | [`not`](logical_operators.md) | `not x` | Logical NOT: `true` if the operand is `false`. |
|
||||
|
||||
The binary arithmetic and bitwise operators also have
|
||||
[compound assignment](/docs/design/assignment.md) forms. These are statements
|
||||
rather than expressions, and do not produce a value.
|
||||
|
||||
## Conversions and casts
|
||||
|
||||
When an expression appears in a context in which an expression of a specific
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
# Assignment statements
|
||||
|
||||
<!--
|
||||
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/2511)
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Abstract](#abstract)
|
||||
- [Problem](#problem)
|
||||
- [Background](#background)
|
||||
- [Proposal](#proposal)
|
||||
- [Details](#details)
|
||||
- [Rationale](#rationale)
|
||||
- [Alternatives considered](#alternatives-considered)
|
||||
- [Allow assignment as a subexpression](#allow-assignment-as-a-subexpression)
|
||||
- [Allow chained assignment](#allow-chained-assignment)
|
||||
- [Do not provide increment and decrement](#do-not-provide-increment-and-decrement)
|
||||
- [Treat increment as syntactic sugar for adding `1`](#treat-increment-as-syntactic-sugar-for-adding-1)
|
||||
- [Define `$` in terms of `$=`](#define--in-terms-of-)
|
||||
- [Do not allow overloading the behavior of `=`](#do-not-allow-overloading-the-behavior-of-)
|
||||
- [Treat the left hand side of `=` as a pattern](#treat-the-left-hand-side-of--as-a-pattern)
|
||||
- [Different names for interfaces](#different-names-for-interfaces)
|
||||
|
||||
<!-- tocstop -->
|
||||
|
||||
## Abstract
|
||||
|
||||
Assignment is permitted only as a complete statement, not as a subexpression.
|
||||
Assignment and compound assignment syntax follow C++ in all other respects.
|
||||
Pre-increment is provided. Post-increment is not. Uses of all of these operators
|
||||
are translated into calls to interface members.
|
||||
|
||||
## Problem
|
||||
|
||||
Assignment is a cornerstone of imperative programming. Carbon does not currently
|
||||
have an approved proposal describing the syntax and interpretation of
|
||||
assignment.
|
||||
|
||||
## Background
|
||||
|
||||
In C-family languages, there are three kinds of assignment-like operators:
|
||||
|
||||
- Simple assignment: `variable = value`
|
||||
- Compound assignment: `variable $= value`, for some binary operators `$`,
|
||||
meaning `variable = variable $ value`, except that `variable` is evaluated
|
||||
only once.
|
||||
- Increment and decrement: `++variable` and `--variable` meaning
|
||||
`variable += 1` and `variable -= 1`, and `variable++` and `variable--`
|
||||
acting similarly but returning the prior value of `variable`.
|
||||
|
||||
These operators behave mostly like other binary operators, and in particular the
|
||||
above expression forms can be used as subexpressions of other expressions.
|
||||
|
||||
Chained assignment is supported, and associates from right to left: `a = b = 1`
|
||||
assigns `1` to `b`, then assigns the result of that assignment to `a`. In C++,
|
||||
that result is typically an lvalue referring to `b`, but in C, it is an rvalue
|
||||
containing the result of converting `1` to the type of `b`. Note that all other
|
||||
operators in C and C++ associate in the other direction.
|
||||
|
||||
These operators have a collection of known issues, including:
|
||||
|
||||
- Confusion between assignment and comparison, for example in constructs such
|
||||
as `if (variable = 3) { ... }`. This is sufficiently rife that a de facto
|
||||
compiler-warning-enforced convention has arisen of using additional
|
||||
parentheses for the rare cases when assignment is intended:
|
||||
`if ((variable = 3))`.
|
||||
- Risk of unsequenced modification and access to the same variable, resulting
|
||||
in undefined behavior. For example, `n = a + n++;` has undefined behavior in
|
||||
C and C++ for this reason, because the act of incrementing `n` is not
|
||||
sequenced with respect to the rest of the computation, including the
|
||||
assignment.
|
||||
- In C++, post-increment can be a performance trap, because it is expected to
|
||||
return the old value of the variable, which might otherwise not be
|
||||
preserved. The additional copying may be optimized away if the `operator++`
|
||||
can be inlined, at the cost of additional work for the compiler.
|
||||
- In C++, overloading an operator `$` does not automatically provide a
|
||||
matching `$=`, resulting in additional work or incomplete operator sets.
|
||||
|
||||
## Proposal
|
||||
|
||||
C-family assignment operators are provided as statements:
|
||||
|
||||
- `variable = value;` is a simple assignment statement.
|
||||
- `variable $= value;` is a compound assignment for each binary operator `$`,
|
||||
other than comparisons. `<=` and `>=` mean "less than or equal to" and
|
||||
"greater than or equal to", not "compare and assign".
|
||||
- `++variable;` and `--variable;` are supported as increment and decrement
|
||||
syntax. Because these are statements, there is no distinction between pre-
|
||||
and post-increment, and post-increment is not provided.
|
||||
|
||||
These operations are translated into calls on interfaces.
|
||||
|
||||
## Details
|
||||
|
||||
See the changes to the design.
|
||||
|
||||
This proposal does not define the semantics of assignment that are provided for
|
||||
classes by default. Leads issue
|
||||
[#686](https://github.com/carbon-language/carbon-lang/issues/686) gives some
|
||||
rules, but those rules are not part of this proposal.
|
||||
|
||||
## Rationale
|
||||
|
||||
- [Language tools and ecosystem](/docs/project/goals.md#language-tools-and-ecosystem)
|
||||
- The values of variables can only change if either the address is taken,
|
||||
including implicitly by `addr` self parameters, or at assignment
|
||||
statements, making it easier to reason about where the value of a
|
||||
variable can change in the control flow of a function.
|
||||
- [Software and language evolution](/docs/project/goals.md#software-and-language-evolution)
|
||||
- This approach is conservative and can evolve to support assignment as a
|
||||
subexpression.
|
||||
- [Code that is easy to read, understand, and write](/docs/project/goals.md#code-that-is-easy-to-read-understand-and-write)
|
||||
- Assignments in subexpressions tend to be hard for humans to read and
|
||||
understand. Disallowing them at the language level avoids the potential
|
||||
for confusion, making code easier to read at the cost of making certain
|
||||
constructs such as `n = arr[i++];` a little harder to write.
|
||||
- Easier to write a complete operator set because both `$` and `$=` can be
|
||||
provided by implementing a single interface.
|
||||
- [Practical safety and testing mechanisms](/docs/project/goals.md#practical-safety-and-testing-mechanisms)
|
||||
- Replaces correctness warning on `if (a = b)` with a language rule.
|
||||
- [Interoperability with and migration from existing C++ code](/docs/project/goals.md#interoperability-with-and-migration-from-existing-c-code)
|
||||
- Providing largely the same set of symbols makes migration easier.
|
||||
- Some cost may be imposed by forcing a translation from assignment in
|
||||
subexpressions to assignment as separate statements.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
### Allow assignment as a subexpression
|
||||
|
||||
We could allow some or all forms of assignment as subexpressions, either with
|
||||
the same syntax or with some other syntax. This proposal does not support
|
||||
assignment as a subexpression because the utility of this feature in C++ is very
|
||||
limited and leads to problems where equality comparison and assignment are
|
||||
easily confused. Modeling assignment as a statement also makes it easier to
|
||||
treat it as the transition point between a variable being in an unformed state
|
||||
and in a fully-formed state, as it prevents such transitions from happening at a
|
||||
not-fully-sequenced point within the evaluation of an expression.
|
||||
|
||||
To avoid the syntactic confusion between assignment and comparison, we could
|
||||
allow assignment as a subexpression with some other syntax. For example, we
|
||||
could adopt Python's "walrus operator" `variable := value`. For now, we are
|
||||
choosing to not pursue this option in order to determine how much motivation
|
||||
there is for such a feature.
|
||||
|
||||
If we allow a three-term form of `for` statements, `for (init; cond; incr)`, we
|
||||
could allow assignment in the `incr` term. However, we currently do not support
|
||||
this syntax.
|
||||
|
||||
The absence of assignment as a subexpression can be worked around with an
|
||||
ergonomic cost, either by using a direct function call `x.(Assign.Op)(y)` or
|
||||
with a lambda wrapping an assignment. Post-increment `a++` could be transformed
|
||||
into `${var b: auto = a; ++a; return b;}`, where `${...}` is a placeholder for
|
||||
Carbon's eventual lambda syntax.
|
||||
|
||||
Workarounds that are not assignment statements would likely be treated as
|
||||
capturing `x` for static analysis purposes, not as definitely initializing `x`,
|
||||
so unformedness checks and other similar checks we might choose to include in
|
||||
the Carbon language design may treat these workarounds conservatively.
|
||||
|
||||
```
|
||||
var a: i32;
|
||||
var b: i32;
|
||||
var c: i32;
|
||||
// ✅ OK, per pending proposal #2006.
|
||||
a = 1;
|
||||
// 🤷 Might result in an error or warning;
|
||||
// `b` is in an unformed state.
|
||||
b.(Assign.Op)(1);
|
||||
// 🤷 Undecided whether we will guarantee that
|
||||
// assignment to `c` precedes read from `c`.
|
||||
a += c.(Assign.Op)(1) + c;
|
||||
```
|
||||
|
||||
### Allow chained assignment
|
||||
|
||||
As a special case of assignment as a subexpression, we could allow chained
|
||||
assignment:
|
||||
|
||||
```
|
||||
a = b = c = 0;
|
||||
```
|
||||
|
||||
We could restrict this to only apply to simple assignment, not cases like
|
||||
`a += b -= c *= 2;`
|
||||
|
||||
In leads issue [#451](https://github.com/carbon-language/carbon-lang/issues/451)
|
||||
it was decided that we would initially not support chained assignment, although
|
||||
this was largely the result of a lack of arguments in favor of support that
|
||||
would justify the complexity of adding a special case, and should be
|
||||
reconsidered if compelling arguments in favor of chained assignment are
|
||||
uncovered.
|
||||
|
||||
### Do not provide increment and decrement
|
||||
|
||||
We could remove `++var;` and `--var;` in favor of `var += 1;` and `var -= 1;`.
|
||||
However, developers coming from C-family languages will expect these operators
|
||||
to exist, and they may more directly convey the intended semantics of counting
|
||||
and navigating in a one-dimensional granular space than a `+=` operation would.
|
||||
|
||||
### Treat increment as syntactic sugar for adding `1`
|
||||
|
||||
We could treat `++a;` as being syntactic sugar for `a += 1;`, in the same way
|
||||
that we treat `a += 1;` as being syntactic sugar for `a = a + 1;`, and similarly
|
||||
for `--a;`. This would mean that floating-point types gain increment and
|
||||
decrement operators, as in C++.
|
||||
|
||||
The literal `1` has its own type, which means that this approach would not
|
||||
require a type to support adding integers in general in order to support
|
||||
increment and decrement, and types such as non-random-access iterators could
|
||||
provide an `it + 1` operation without exposing a non-constant-time `it + n`
|
||||
operation.
|
||||
|
||||
One potential advantage of this approach is that a generic constraint that is
|
||||
sufficient to allow `a = a + 1;` to type-check would also allow `++a;` to
|
||||
type-check. For example, `T:! Assign & Add where i32 is ImplicitAs(.Self)` would
|
||||
suffice to allow such an increment, as would a facet type with a narrower
|
||||
`ImplicitAs` constraint that permits only the literal `1`. However, this benefit
|
||||
is minor, given that writing `a += 1;` instead is not a major burden.
|
||||
|
||||
The cost of making this change would be that the semantics of increment and
|
||||
decrement are tied to a very numerical notion of "adding 1". That may not be
|
||||
appropriate for all types that want to support a more general notion of "move to
|
||||
the next value" or "move to the previous value", such as a more generalized
|
||||
notion of cursor. It may also not be appropriate for all types that support
|
||||
addition of exactly 1 to support increment; for example:
|
||||
|
||||
- For floating-point types, adding exactly 1 does not necessarily produce a
|
||||
different number, and there is a different meaningful notion of "move to the
|
||||
next value" -- namely, moving to the next _representable_ value -- which may
|
||||
be intended instead. In C++ code, where increment of floating-point types is
|
||||
permitted, it is vanishingly rare.
|
||||
- For complex numbers, for example in a Gaussian integer type, having `++c;`
|
||||
move one unit in the real direction seems arbitrary.
|
||||
- For a rational number type, as for floating-point types, adding exactly 1
|
||||
seems unlikely to be a common "navigation" operation, even though there is
|
||||
no other reasonable notion of "move to the next value".
|
||||
|
||||
### Define `$` in terms of `$=`
|
||||
|
||||
Instead of defining `$=` in terms of `$` and `=`, we could define `$` in terms
|
||||
of `$=` and copying. Our experience from C++ is that `$=` can frequently be
|
||||
implemented more efficiently than `$`, by operating in-place and reusing
|
||||
allocated storage from the left-hand operand, so this might be a better default.
|
||||
|
||||
There are a few reasons why we choose to not do this:
|
||||
|
||||
- The direction in this proposal is expected to be less surprising. Defining
|
||||
`$=` in terms of `$` and `=` seems more in line with programmer expectations
|
||||
based both on the morphology of the token and on how it is generally taught.
|
||||
- Under the rules in this proposal, an `Add & Assign` constraint ends up being
|
||||
effectively equivalent to an `AddAssign` constraint, due to the blanket
|
||||
implementation of `AddAssign` in terms of `Add` and `Assign`. This means
|
||||
that constraining a type to provide both `Add` and `Assign` is sufficient to
|
||||
use `+=`, which seems desirable. If the defaults were reversed, this would
|
||||
not be achievable.
|
||||
- If the reverse rule were adopted, two separate `impl`s would still be
|
||||
required in order to permit implicit conversions on the left hand side of
|
||||
the `$` for a type, but not on the left hand side of a `$=`.
|
||||
- There are cases where `$` can be implemented more efficiently than making a
|
||||
copy and performing a `$=` operation. For example, if the type is large,
|
||||
implementing `$` in terms of `$=` can require two passes over the
|
||||
destination instead of one, which may increase the constant factor
|
||||
performance of the operation.
|
||||
|
||||
### Do not allow overloading the behavior of `=`
|
||||
|
||||
We could define that an `=` expression always carries out these steps:
|
||||
|
||||
- Initialize a value of the left-hand operand's type from the right-hand
|
||||
operand.
|
||||
- Destroy the left-hand operand.
|
||||
- Move the value created earlier into the left-hand operand.
|
||||
|
||||
However, this removes some flexibility and could harm performance, for example
|
||||
in the case where the left-hand operand has a buffer that it could reuse to
|
||||
store its new value. This would also be a significant deviation from C++, where
|
||||
some types take advantage of this additional flexibility, and would be expected
|
||||
to harm interoperability and migration.
|
||||
|
||||
### Treat the left hand side of `=` as a pattern
|
||||
|
||||
We could allow pattern-like syntax on the left hand side of `=` instead of an
|
||||
expression.
|
||||
|
||||
```
|
||||
fn GCD(var a: i32, var b: i32) -> i32 {
|
||||
if (b < a) {
|
||||
// Swap `a` and `b`.
|
||||
(a, b) = (b, a);
|
||||
}
|
||||
while (a != 0) {
|
||||
// Calculate both `b % a` and `a`,
|
||||
// then assign to both `a` and `b`.
|
||||
(a, b) = (b % a, a);
|
||||
}
|
||||
return b;
|
||||
}
|
||||
```
|
||||
|
||||
However, this would be a novel interpretation of pattern syntax: there is no
|
||||
mechanism in the current pattern syntax to assign to an existing variable.
|
||||
Pattern-matching `(a, b)` against `(b % a, a)` would instead compare `a` to
|
||||
`b % a` and compare `b` to `a` in the current mechanism. It is not clear that
|
||||
this level of novelty is justified by the value added by this functionality.
|
||||
|
||||
### Different names for interfaces
|
||||
|
||||
We considered various different names for the interfaces in this proposal.
|
||||
Differences from the proposed names are highlighted:
|
||||
|
||||
- `Assign`, `AssignWith`, `OpAssign`, `OpAssignWith` (proposed)
|
||||
|
||||
- Consistently uses `With` suffix to describe the right-hand type.
|
||||
- These names have a direct connection to the lexical operator syntax: the
|
||||
interface for `$=` is named as the interface for `$` followed by the
|
||||
interface for `=`.
|
||||
- The word order in the name describes the order in which the operations
|
||||
are notionally performed: first `Op`, then `Assign`.
|
||||
- Compound assignment interfaces will group alphabetically with the
|
||||
corresponding operator, rather than with assignments, which is likely to
|
||||
be better for people searching for items in a sorted list.
|
||||
- Matches the choice made in Rust.
|
||||
|
||||
- `Assign`, **_`AssignFrom`_**, `OpAssign`, `OpAssignWith`
|
||||
|
||||
- This reads more naturally in English. `With` isn't really the right word
|
||||
to use in this context, and may be confusing.
|
||||
- Violates the consistency of using `...With` for all the parameterized
|
||||
operator interfaces.
|
||||
- Given how common this interface is expected to be compared to the rest,
|
||||
the inconsistency of using `AssignFrom` might be acceptable, but for now
|
||||
we will use `AssignWith`. If there are sustained concerns with this name
|
||||
(if we don't "get used to it"), we should reconsider.
|
||||
|
||||
- `Assign`, `AssignWith`, **_`AssignOp`_**, **_`AssignOpWith`_**
|
||||
|
||||
- Consistently uses `With` suffix to describe the right-hand type.
|
||||
- The name `AssignOpWith(U)` decomposes as `Assign` + `OpWith(U)` in a way
|
||||
that describes the two operations being performed.
|
||||
- When written as function calls, the behavior is `Assign(x, Op(x, y))`,
|
||||
which again uses the `AssignOp` word order.
|
||||
|
||||
- `Assign`, **_`AssignGiven`_**, `OpAssign`, **_`OpAssignGiven`_**
|
||||
|
||||
- `Given` might be a less surprising word for simple assignment than
|
||||
`With`, but is still not ideal.
|
||||
- This choice seems slightly worse in most existing cases that use `With`,
|
||||
and we didn't consider the benefit of `AssignGiven` over `AssignWith` to
|
||||
be sufficient to justify that cost.
|
||||
|
||||
- `Assign`, `AssignWith` or **_`AssignFrom`_**, **_`InPlaceOp`_**,
|
||||
**_`InPlaceOpWith`_**
|
||||
|
||||
- Might make `AssignFrom` more viable by making `Assign` / `AssignFrom` no
|
||||
longer parallel `InPlaceOp` / `InPlaceOpWith` so closely.
|
||||
- Might better match how these operations are described in everyday
|
||||
parlance.
|
||||
|
||||
Overall, the proposed set of names seem like the best choice, despite some of
|
||||
the names not reading completely naturally. The English readability concern is
|
||||
probably not much worse than for `LeftShiftWith`, where we already decided that
|
||||
consistency was more important.
|
||||
Reference in New Issue
Block a user