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:
Richard Smith
2023-01-27 19:03:47 -08:00
committed by GitHub
co-authored by Jon Ross-Perkins Chandler Carruth
parent 6a59c77715
commit a89aba2698
5 changed files with 739 additions and 28 deletions
+6 -26
View File
@@ -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)`
+357
View File
@@ -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)
+3 -2
View File
@@ -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
+4
View File
@@ -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
+369
View File
@@ -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.