Assignment

Table of contents

Overview

Values can be assigned to variables using the = operator:

var a: i32 = 5;
a = 6;

For each binary arithmetic or bitwise 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 provided as part of the Carbon standard library.

Syntax

The operands of these operators can be any expression. 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 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 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

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 { extend 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 { extend AddAssignWith(Self); }
// Compound `-=`.
interface SubAssignWith(U:! type) {
  fn Op[addr self: Self*](other: U);
}
constraint SubAssign { extend SubAssignWith(Self); }
// Compound `*=`.
interface MulAssignWith(U:! type) {
  fn Op[addr self: Self*](other: U);
}
constraint MulAssign { extend MulAssignWith(Self); }
// Compound `/=`.
interface DivAssignWith(U:! type) {
  fn Op[addr self: Self*](other: U);
}
constraint DivAssign { extend DivAssignWith(Self); }
// Compound `%=`.
interface ModAssignWith(U:! type) {
  fn Op[addr self: Self*](other: U);
}
constraint ModAssign { extend 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 { extend BitAndAssignWith(Self); }
// Compound `|=`.
interface BitOrAssignWith(U:! type) {
  fn Op[addr self: Self*](other: U);
}
constraint BitOrAssign { extend BitOrAssignWith(Self); }
// Compound `^=`.
interface BitXorAssignWith(U:! type) {
  fn Op[addr self: Self*](other: U);
}
constraint BitXorAssign { extend BitXorAssignWith(Self); }
// Compound `<<=`.
interface LeftShiftAssignWith(U:! type) {
  fn Op[addr self: Self*](other: U);
}
constraint LeftShiftAssign { extend LeftShiftAssignWith(Self); }
// Compound `>>=`.
interface RightShiftAssignWith(U:! type) {
  fn Op[addr self: Self*](other: U);
}
constraint RightShiftAssign { extend 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 impls 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

References