Bitwise operators (#1191)

Add bitwise and bit-shift operators `&`, `|`, `^`, `<<`, `>>`. Replace C++ `~` with unary prefix `^`.

Co-authored-by: Chandler Carruth <chandlerc@gmail.com>
This commit is contained in:
Richard Smith
2022-05-06 14:26:55 -07:00
committed by GitHub
co-authored by Chandler Carruth
parent 0a93289706
commit c4fecf720f
5 changed files with 753 additions and 11 deletions
+37 -5
View File
@@ -65,6 +65,12 @@ graph BT
click memberAccess "https://github.com/carbon-language/carbon-lang/blob/trunk/docs/design/expressions/member_access.md"
negation["-x"]
click negation "https://github.com/carbon-language/carbon-lang/blob/trunk/docs/design/expressions/arithmetic.md"
complement["^x"]
click complement "https://github.com/carbon-language/carbon-lang/blob/trunk/docs/design/expressions/bitwise.md"
unary((" "))
as["x as T"]
click as "https://github.com/carbon-language/carbon-lang/blob/trunk/docs/design/expressions/implicit_conversions.md"
@@ -80,6 +86,17 @@ graph BT
modulo["x % y"]
click modulo "https://github.com/carbon-language/carbon-lang/blob/trunk/docs/design/expressions/arithmetic.md"
bitwise_and>"x & y"]
bitwise_or>"x | y"]
bitwise_xor>"x ^ y"]
click bitwise_and "https://github.com/carbon-language/carbon-lang/blob/trunk/docs/design/expressions/bitwise.md"
click bitwise_or "https://github.com/carbon-language/carbon-lang/blob/trunk/docs/design/expressions/bitwise.md"
click bitwise_xor "https://github.com/carbon-language/carbon-lang/blob/trunk/docs/design/expressions/bitwise.md"
shift["x << y<br>
x >> y"]
click shift "https://github.com/carbon-language/carbon-lang/blob/trunk/docs/design/expressions/bitwise.md"
comparison["x == y<br>
x != y<br>
x < y<br>
@@ -91,12 +108,16 @@ graph BT
not["not x"]
click not "https://github.com/carbon-language/carbon-lang/blob/trunk/docs/design/expressions/logical_operators.md"
logicalOperand((" "))
and>"x and y"]
click and "https://github.com/carbon-language/carbon-lang/blob/trunk/docs/design/expressions/logical_operators.md"
or>"x or y"]
click or "https://github.com/carbon-language/carbon-lang/blob/trunk/docs/design/expressions/logical_operators.md"
logicalExpression((" "))
if>"if x then y else z"]
click if "https://github.com/carbon-language/carbon-lang/blob/trunk/docs/design/expressions/if.md"
@@ -104,18 +125,23 @@ graph BT
memberAccess --> parens & braces & unqualifiedName
negation --> memberAccess
complement --> memberAccess
unary --> negation & complement
%% Use a longer arrow here to put `not` next to `and` and `or`.
not -----> memberAccess
multiplication & modulo & as --> negation
multiplication & modulo & as & bitwise_and & bitwise_or & bitwise_xor & shift --> unary
addition --> multiplication
comparison --> modulo & addition & as
and & or --> comparison & not
if & expressionEnd --> and & or
comparison --> modulo & addition & as & bitwise_and & bitwise_or & bitwise_xor & shift
logicalOperand --> comparison & not
and & or --> logicalOperand
logicalExpression --> and & or
if & expressionEnd --> logicalExpression
```
The diagram's attributes are:
- Each node represents a precedence group.
- Each non-empty node represents a precedence group. Empty circles are used to
simplify the graph, and do not represent a precedence group.
- When an expression is composed from different precedence groups, the
interpretation is determined by the precedence edges:
@@ -221,11 +247,17 @@ Most expressions are modeled as operators:
| Category | Operator | Syntax | Function |
| ---------- | ------------------------------- | --------- | --------------------------------------------------------------------- |
| Arithmetic | [`-`](arithmetic.md) (unary) | `-x` | The negation of `x`. |
| Bitwise | [`^`](bitwise.md) (unary) | `^x` | The bitwise complement of `x`. |
| Arithmetic | [`+`](arithmetic.md) | `x + y` | The sum of `x` and `y`. |
| Arithmetic | [`-`](arithmetic.md) (binary) | `x - y` | The difference of `x` and `y`. |
| Arithmetic | [`*`](arithmetic.md) | `x * y` | The product of `x` and `y`. |
| Arithmetic | [`/`](arithmetic.md) | `x / y` | `x` divided by `y`, or the quotient thereof. |
| Arithmetic | [`%`](arithmetic.md) | `x % y` | `x` modulo `y`. |
| Bitwise | [`&`](bitwise.md) | `x & y` | The bitwise AND of `x` and `y`. |
| Bitwise | [`\|`](bitwise.md) | `x \| y` | The bitwise OR of `x` and `y`. |
| Bitwise | [`^`](bitwise.md) (binary) | `x ^ y` | The bitwise XOR of `x` and `y`. |
| Bitwise | [`<<`](bitwise.md) | `x << y` | `x` bit-shifted left `y` places. |
| Bitwise | [`>>`](bitwise.md) | `x >> y` | `x` bit-shifted right `y` places. |
| Conversion | [`as`](as_expressions.md) | `x as T` | Converts the value `x` to the type `T`. |
| Comparison | [`==`](comparison_operators.md) | `x == y` | Equality: `true` if `x` is equal to `y`. |
| Comparison | [`!=`](comparison_operators.md) | `x != y` | Inequality: `true` if `x` is not equal to `y`. |
+16 -6
View File
@@ -55,11 +55,17 @@ standard library.
## Precedence and associativity
```mermaid
graph TD
negation["-x"] --> multiplicative & modulo
multiplicative>"x * y<br> x / y"] --> additive
additive>"x + y<br> x - y"]
%%{init: {'themeVariables': {'fontFamily': 'monospace'}}}%%
graph BT
negation["-x"]
multiplication>"x * y<br>
x / y"]
addition>"x + y<br>
x - y"]
modulo["x % y"]
multiplication & modulo --> negation
addition --> multiplication
```
<small>[Instructions for reading this diagram.](README.md#precedence)</small>
@@ -153,8 +159,12 @@ programming errors:
will be aborted, or the arithmetic will evaluate to a mathematically
incorrect result, such as a two's complement result or zero. The program
might not in all cases be aborted immediately -- for example, multiple
overflow checks might be combined into one, and if the result of an
arithmetic operation is never observed, the abort may not happen at all.
overflow checks might be combined into one -- but no control flow or memory
access that depends on the value will be performed.
**TODO:** Unify the description of these programming errors with those of
bit-shift domain errors, document the behavior in a common place and link to it
from here.
**TODO:** In a hardened build, should we prefer to trap on overflow, give a
two's complement result, or produce zero? Using zero may defeat some classes of
+283
View File
@@ -0,0 +1,283 @@
# Bitwise and shift operators
<!--
Part of the Carbon Language project, under the Apache License v2.0 with LLVM
Exceptions. See /LICENSE for license information.
SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
-->
<!-- toc -->
## Table of contents
- [Overview](#overview)
- [Precedence and associativity](#precedence-and-associativity)
- [Integer types](#integer-types)
- [Integer constants](#integer-constants)
- [Extensibility](#extensibility)
- [Alternatives considered](#alternatives-considered)
- [References](#references)
<!-- tocstop -->
## Overview
Carbon provides a conventional set of operators for operating on bits:
```
var a: u8 = 5;
var b: u8 = 3;
var c: i8 = -5;
// 250
var complement: u8 = ^a;
// 1
var bitwise_and: u8 = a & b;
// 7
var bitwise_or: u8 = a | b;
// 6
var bitwise_xor: u8 = a ^ b;
// 40
var left_shift: u8 = a << b;
// 2
var logical_right_shift: u8 = a >> 1;
// -3
var arithmetic_right_shift: i8 = c >> 1;
```
These operators have [predefined meanings](#integer-types) for Carbon's integer
types. User-defined types can define the meaning of these operations by
[implementing an interface](#extensibility) provided as part of the Carbon
standard library.
## Precedence and associativity
```mermaid
%%{init: {'themeVariables': {'fontFamily': 'monospace'}}}%%
graph BT
complement["^x"]
bitwise_and>"x & y"]
bitwise_or>"x | y"]
bitwise_xor>"x ^ y"]
shift["x << y<br>
x >> y"]
bitwise_and & bitwise_or & bitwise_xor & shift --> complement
```
<small>[Instructions for reading this diagram.](README.md#precedence)</small>
Parentheses are required when mixing different bitwise and bit-shift operators.
Binary `&`, `|`, and `^` are left-associative. The bit-shift operators `<<` and
`>>` are non-associative.
```
// ✅ Same as (1 | 2) | 4, evaluates to 7.
var a: i32 = 1 | 2 | 4;
// ❌ Error, parentheses are required to distinguish between
// (3 | 5) & 6, which evaluates to 6, and
// 3 | (5 & 6), which evaluates to 7.
var b: i32 = 3 | 5 & 6;
// ❌ Error, parentheses are required to distinguish between
// (1 << 2) << 3, which evaluates to 4 << 3 == 32, and
// 1 << (2 << 3), which evaluates to 1 << 16 == 65536.
var c: i32 = 1 << 2 << 3;
// ❌ Error, can't repeat the `^` operator. Use `^(^4)` or simply `4`.
var d: i32 = ^^4;
```
## Integer types
Bitwise and bit-shift operators are supported for Carbon's built-in integer
types, and, unless that behavior is [overridden](#extensibility), for types that
can be implicitly converted to integer types, as follows:
For binary bitwise operators, if one operand has an integer type and the other
operand can be implicitly converted to that type, then it is. If both operands
are of integer type, this results in the following conversions:
- If the types are `uN` and `uM`, or they are `iN` and `iM`, the operands are
converted to the larger type.
- If one type is `iN` and the other type is `uM`, and `M` < `N`, the `uM`
operand is converted to `iN`.
A built-in binary bitwise `&`, `|`, or `^` operation is performed if, after the
above conversion step, the operands have the same integer type. The result type
is that type, and the result value is produced by applying the relevant
operation -- AND, OR, or XOR -- to each pair of corresponding bits in the input,
including the sign bit for a signed integer type.
A built-in complement operation is performed if the operand can be implicitly
converted to an integer type. The result type is that type, and the result value
is produced by flipping all bits in the input, including the sign bit for a
signed integer type. `^a` is equivalent to `a ^ x`, where `x` is the
all-one-bits value of the same type as `a`.
A built-in bit-shift operation is performed if both operands are, or can be
implicitly converted to, integer types. The result type is the converted type of
the first operand. The result value is produced by shifting the first operand
left for `<<` or right for `>>` a number of positions equal to the second
operand. Vacant positions are filled with `0` bits, except for a right shift
where the first operand is of a signed type and has a negative value, in which
case they are filled with `1` bits.
For the purposes of bit-shifts, bits are ordered by significance, with the most
significant bit being the leftmost bit and the least significant bit being the
rightmost bit. As a consequence, in the absence of overflow a left shift is
equivalent to a multiplication by a power of 2 and a right shift is equivalent
to a division by a power of two, rounding downwards.
The second operand of a bit-shift is required to be between zero (inclusive) and
the bit-width of the first operand (exclusive); it is a programming error if the
second operand is not within that range.
- In a development build, they will be caught immediately when they happen at
runtime.
- In a performance build, the optimizer may assume that this programming error
does not occur.
- In a hardened build, the result will have well the defined behavior of
either aborting the program or performing a shift of an unspecified number
of bits, which if wider than the first operand will result in `0` or `-1`.
In the case where the program is aborted, the program might not in all cases
be aborted immediately -- for example, multiple checks might be combined
into one -- but no control flow or memory access that depends on the value
will be performed.
**TODO:** Unify the description of these programming errors with those of
arithmetic overflow, document the behavior in a common place and link to it from
here.
## Integer constants
These operations can also be applied to a pair of integer constants, or to an
integer constant and a value of integer type, as follows:
- If any binary bitwise or bit-shift operator is applied to two integer
constants, or the unary `^` operator is applied to an integer constant, the
result is an integer constant. Integer constants are treated as having
infinitely many high-order bits, where all but finitely many of those bits
are sign bits. For example, `-1` comprises infinitely many `1` bits. Note
that there is no difference between an arithmetic and a logical right shift
on an integer constant, because every bit always has a higher-order bit to
shift from.
- It is easy to produce extremely large numbers by left-shifting an
integer constant. For example, the binary representation of
`1 << (1 << 1000)` is thought to be substantially larger than the total
entropy in the observable universe. In practice, Carbon implementations
will set a much lower limit on the largest integer constant that they
support.
- If a binary bitwise `&`, `|`, or `^` operation is applied to an integer
constant and a value of an integer type to which the constant can be
implicitly converted, the operand that is an integer constant is implicitly
converted to the integer type and the computation is performed as described
[above](#integer-types).
- If the second operand of a bit-shift operator is an integer constant and the
first opreand is not, and the second operand is between 0 (inclusive) and
the bit-width of the first operand (exclusive), the integer constant is
converted to an integer type that can hold its value and the computation is
performed as described above.
Other operations involving integer constants are invalid. For example, a bitwise
`&` between a `u8` and an integer constant `500` is invalid because `500`
doesn't fit into `u8`, and `1 << n` is invalid if `n` is an integer variable
because we don't know what type to use to compute the result.
Note that the unary `^` operator applied to a non-negative integer constant
results in a negative integer constant, and the binary `^` operator gives a
negative result if exactly one of the input operands was negative. For example,
`^0 == -1` evaluates to `true`.
## Extensibility
Bitwise and shift operators can be provided for user-defined types by
implementing the following family of interfaces:
```
// Unary `^`.
interface BitComplement {
let Result:! Type = Self;
fn Op[me: Self]() -> Result;
}
```
```
// Binary `&`.
interface BitAndWith(U:! Type) {
let Result:! Type = Self;
fn Op[me: Self](other: U) -> Result;
}
constraint BitAnd {
extends BitAndWith(Self) where .Result = Self;
}
```
```
// Binary `|`.
interface BitOrWith(U:! Type) {
let Result:! Type = Self;
fn Op[me: Self](other: U) -> Result;
}
constraint BitOr {
extends BitOrWith(Self) where .Result = Self;
}
```
```
// Binary `^`.
interface BitXorWith(U:! Type) {
let Result:! Type = Self;
fn Op[me: Self](other: U) -> Result;
}
constraint BitXor {
extends BitXorWith(Self) where .Result = Self;
}
```
```
// Binary `<<`.
interface LeftShiftWith(U:! Type) {
let Result:! Type = Self;
fn Op[me: Self](other: U) -> Result;
}
constraint LeftShift {
extends LeftShiftWith(Self) where .Result = Self;
}
```
```
// Binary `>>`.
interface RightShiftWith(U:! Type) {
let Result:! Type = Self;
fn Op[me: Self](other: U) -> Result;
}
constraint RightShift {
extends RightShiftWith(Self) where .Result = Self;
}
```
Given `x: T` and `y: U`:
- The expression `^x` is rewritten to `x.(BitComplement.Op)()`.
- The expression `x & y` is rewritten to `x.(BitAndWith(U).Op)(y)`.
- The expression `x | y` is rewritten to `x.(BitOrWith(U).Op)(y)`.
- The expression `x ^ y` is rewritten to `x.(BitXorWith(U).Op)(y)`.
- The expression `x << y` is rewritten to `x.(LeftShiftWith(U).Op)(y)`.
- The expression `x >> y` is rewritten to `x.(RightShiftWith(U).Op)(y)`.
Implementations of these interfaces are provided for built-in types as necessary
to give the semantics described above.
## Alternatives considered
- [Use different symbols for bitwise operators](/proposals/p1191.md#use-different-symbols-for-bitwise-operators)
- [Provide different operators for arithmetic and logical shifts](/proposals/p1191.md#provide-different-operators-for-arithmetic-and-logical-shifts)
- [Provide rotate operators](/proposals/p1191.md#provide-rotate-operators)
- [Guarantee the behavior of large shifts](/proposals/p1191.md#guarantee-behavior-of-large-shifts)
- [Support shifting a constant by a variable](/proposals/p1191.md#support-shifting-a-constant-by-a-variable)
## References
- Proposal
[#1191: bitwise and shift operators](https://github.com/carbon-language/carbon-lang/pull/1191).
@@ -140,6 +140,21 @@ except:
have singleton types; see issue
[#508](https://github.com/carbon-language/carbon-lang/issues/508).
In addition to the above rules, a negative integer constant `k` can be
implicitly converted to the type `uN` if the value `k` + 2<sup>N</sup> can be
exactly represented, and converts to that value. Note that this conversion
violates the "semantics-preserving" test. For example, `(-1 as u8) as i32`
produces the value `255` whereas `-1 as i32` produces the value `-1`. However,
this conversion is important in order to allow bitwise operations with masks, so
we allow it:
```
// We allow ^0 == -1 to convert to `u32` to represent an all-ones value.
var a: u32 = ^0;
// ^4 == -5 is negative, but we want to allow it to convert to u32 here.
var b: u32 = a & ^4;
```
### Same type
The following conversion is available for every type `T`:
@@ -214,6 +229,7 @@ types.
- [Provide no implicit conversions](/proposals/p0820.md#no-conversions)
- [Provide no extensibility](/proposals/p0820.md#no-extensibility)
- [Apply implicit conversions transitively](/proposals/p0820.md#transitivity)
- [Do not allow negative constants to convert to unsigned types](/proposals/p1191.md#converting-complements-to-unsigned-types)
## References
+401
View File
@@ -0,0 +1,401 @@
# Bitwise and shift operators
<!--
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/1191)
<!-- toc -->
## Table of contents
- [Problem](#problem)
- [Background](#background)
- [Overflow in shift operators](#overflow-in-shift-operators)
- [Proposal](#proposal)
- [Details](#details)
- [Rationale](#rationale)
- [Alternatives considered](#alternatives-considered)
- [Use different symbols for bitwise operators](#use-different-symbols-for-bitwise-operators)
- [Use a multi-character spelling](#use-a-multi-character-spelling)
- [Don't provide an xor operator](#dont-provide-an-xor-operator)
- [Use `~`, or some other symbol, for complement](#use--or-some-other-symbol-for-complement)
- [Provide different operators for arithmetic and logical shifts](#provide-different-operators-for-arithmetic-and-logical-shifts)
- [Provide rotate operators](#provide-rotate-operators)
- [Guarantee behavior of large shifts](#guarantee-behavior-of-large-shifts)
- [Support shifting a constant by a variable](#support-shifting-a-constant-by-a-variable)
- [Converting complements to unsigned types](#converting-complements-to-unsigned-types)
<!-- tocstop -->
## Problem
Carbon needs operations for working with bit representations of values.
## Background
C++ provides four bitwise operations for Boolean algebra: complement (`~`), and
(`&`), or (`|`), and xor (`^`). These are all useful in bit-maipulation code
(although `^` is used substantially less than the others). In addition, C++
provides two bit-shift operators `<<` and `>>` that can perform three different
operations: left shift, arithmetic right shift, and logical right shift. The
meaning of `>>` is determined by the signedness of the first operand.
C and Swift use the same set of operators as C++. Rust uses most of the same
operators, but uses `!` instead of `~` for complement, unifying it with the
logical not operator, which is spelled `not` in Carbon and as `!` in Rust and
C++. Go uses most of the same operators as C++, but uses unary prefix `^`
instead of `~` for complement, mirroring binary `^` for xor.
In addition to the operators provided by C and C++, bit-rotate operators are
present in some languages, and a short notation for them may be convenient.
Finally, there are other non-degenerate binary Boolean operations with no common
operator symbol:
- The "implies" operator (equivalent to `~a | b`).
- The "implied by" operator (equivalent to `a | ~b`).
- The complement of each of the other operators (NAND, NOR, XNOR, "does not
imply", "is not implied by").
### Overflow in shift operators
The behavior of shift operators in C++ had a turbulent past. The behavior of
shift operators has always been undefined if the right operand is out of range
-- not between zero inclusive and the bit-width of the left operator exclusive
-- but other restrictions have varied:
- Unsigned left shift has never had any restrictions on the first operand.
- For signed left shift:
- In C++98, the result was fully unspecified.
- In C++11, the result was specified only if the first operand was
non-negative and the result fit into the destination type -- that is, if
no 1 bit is shifted into the sign bit.
- In C++14, the result was specified only if the first operand was
non-negative and the result fit into the unsigned type corresponding to
the destination type -- that is, if no 1 bit is shifted out of the sign
bit.
- In C++20 onwards, there are no restrictions beyond a range restriction
on the right operand, and the result is otherwise always specified, even
if the left operand is negative.
- Unsigned right shift has never had any restrictions on the first operand.
- For signed right shift:
- In C++17 and earlier, if the left operand is negative, the result is
implementation-defined.
- In C++20 onwards, the result is always specified, even if the left
operand is negative.
There is a clear trend towards defining more cases, following two's complement
rules.
## Proposal
Use the same operator set as C++, but replace `~` with unary prefix `^`.
Define the behavior for all cases of `<<` and `>>` except where the right
operand is either negative or is greater than or equal to the bit-width of the
left operand.
## Details
See changes to the design, and in particular
[the new section on bitwise and shift operators](/docs/design/expressions/bitwise.md).
## Rationale
- [Performance-critical software](/docs/project/goals.md#performance-critical-software)
- Bit operations are important low-level primitives. Providing operators
for them is important in order to allow low-level high-performance code
to be written elegantly in Carbon.
- By not defining semantics for `<<` and `>>` when the right-hand operand
is out of range, we can directly use hardware instructions for these
operations whose behavior in these cases vary between architectures.
- [Code that is easy to read, understand, and write](/docs/project/goals.md#code-that-is-easy-to-read-understand-and-write)
- Using operator notation rather than function call notation for bitwise
operators improves readability in code making heavy use of these
operations.
- [Practical safety and testing mechanisms](/docs/project/goals.md#practical-safety-and-testing-mechanisms)
- Carbon follows C++ in treating `<<` and `>>` as programming errors when
the right hand operand is out of range, but Carbon guarantees that such
errors will not directly result in unbounded misbehavior in hardened
builds.
- [Modern OS platforms, hardware architectures, and environments](/docs/project/goals.md#modern-os-platforms-hardware-architectures-and-environments)
- All hardware architectures we care to support are natively two's
complement architectures, and that assumption allows us to define the
semantics of shift operators in the way that makes the most sense for
such architectures.
- Our bitwise operations make no assumptions about the endianness of the
hardware architecture, although the shift operators make the most sense
on a little-endian or big-endian architecture, which are the only
endiannesses we expect to see in modern hardware platforms.
- [Interoperability with and migration from existing C++ code](/docs/project/goals.md#interoperability-with-and-migration-from-existing-c-code)
- The same set of operators is provided as in C++, making it easy for
programmers and programs to migrate, with the exception that the `~`
operator is mechanically replaced with `^`. This change is expected to
be easy for programmers to accommodate, especially given that Rust's
choice to replace `~` with `!` does not seem to be a source of sustained
complaints.
- The extensiblity support reflects the full scope of operator overloading
in C++, permitting separate overloading of each of the bitwise operators
with custom return types. This should allow smooth interoperability with
C++ overloaded operators.
## Alternatives considered
### Use different symbols for bitwise operators
The operator syntax for bitwise operators was decided in
[#545](https://github.com/carbon-language/carbon-lang/issues/545). Some of the
specific alternatives considered are discussed below.
#### Use a multi-character spelling
We considered using various multi-character spellings for the bitwise and, or,
xor, and complement operators:
- `&:`, `|:`, `^:`, `~:`
- `.&.`, `.|.`, `.^.`, `.~.`
- `.*.`, `.+.`, `.!=.`, `.!.`
- `/\`, `\/`, `(+)`, `-|`
- `bitand`, `bitor`, `bitxor`, `compl`
The advantage of switching to such a set of operators is that this would free up
the single-character `&`, `|`, `^`, and `~` tokens for other uses that may occur
more frequently in Carbon programs. We have some candidate uses for these
operators already:
- `&` is used for combining interfaces and as a unary address-of operator.
- `|` may be useful for sum types, for alternatives in patterns, or as another
kind of bracket as in
[Ruby's lambda notation](https://ruby-doc.org/docs/ruby-doc-bundle/Manual/man-1.4/syntax.html#iter).
- `~` may be useful as a destructive move notation.
- `^` may be useful as a postfix pointer dereference operator.
Other motivations for switching to a different set of spellings include:
- There are some concerns that `<` and `<<` are visually similar, analogous to
`&` and `&&`.
- Carbon has moved away from `&&` and other punctuation based _logical_
operators and towards keywords like `and`. Bitwise operators could similarly
switch to keywords like `bitand`.
However, moving substantially away from the C++ operator set comes with a set of
concerns:
- There are strong established expectations and intuitions about these
operators and their spellings among C++ practitioners.
- In some of the code that uses these operators, they are used a lot, and a
more cumbersome operator may consequently cause an outsized detriment on
readability.
- These operations are used particularly in the area of low-level,
high-performance code, which is an area for which we want Carbon to be
especially appealing. Using short operators for these operations
demonstrates our commitment to providing good support for such code.
- Even if we didn't use these operators as bit operators, we would still want
to exercise caution when using them for some other purpose to avoid
surprising C++ developers.
- While some visual similarity exists such as between `<` and `<<`, the
contexts in which these operators are used are sufficiently different to
avoid any serious concerns.
- The primary motivation of using `and` instead of `&&` doesn't apply for
bitwise operators: the _logical_ operator represents _control flow_.
Separating logical and bitwise "and" operations more visibly seems
especially important because of this control flow semantic difference.
Without any control flow and with the keywords being significantly longer
for bitwise operations, the above considerations were the dominant ones that
led us to stick with familiar `&` and `|` spellings.
#### Don't provide an xor operator
We considered omitting the `^` operator, providing this functionality in some
other way, such as a named function or an `xor` keyword, while keeping the `&`
and `|` symbols for bitwise operations. We could take a similar approach for the
complement operation, such as by using a `compl` keyword. The primary motivation
was to avoid spending two precious operator characters on relatively uncommon
operations. However, we did not want to apply the same change to `&` and `|`,
and it seemed important to maintain consistency between the three binary bitwise
operators from C++.
Using `^` for both operations provides some of the benefits here, allowing us to
reclaim `~`, without introducing the inconsistency that would result from using
keywords.
#### Use `~`, or some other symbol, for complement
We could follow C++ and use `~` as the complement operator. However, using `~`
for this purpose spends a precious operator character on a relatively uncommon
operation, and `~` is often visually confusible with `-`, creating the potential
for readability problems. Also, in C++, `~` is overloaded to also refer to
destruction, and we may want to use the same notation for destruction or
destructive move semantics in Carbon.
We found `^` to be a satisfying alternative with a good rationale and mnemonic:
`^` is a bit-flipping operator -- `a ^ b` flips the bits in `a` that are
specified in `b` -- and complement is an operator that flips _all_ the bits.
`^a` is equivalent to `a ^ n`, where `n` is the all-one-bits value in the type
of `a`.
We also considered using `!` for complement, like Rust does. Unlike in Rust,
this would not be a generalization of `!` on `bool`, because we use `not` for
`bool` negation, and repurposing `!` in this way compared to C++ seemed
confusing.
### Provide different operators for arithmetic and logical shifts
We could provide different operators for arithmetic right shift and logical
right shift. This might allow programmers to better express their intent.
However, it seems unnecessary, as using the type of the left operand is a
strategy that doesn't appear to have caused significant problems in practice in
the languages that have followed it.
Basing the kind of shift on the signedness of the left operand also follows from
viewing a negative number as having an infinite number of leading 1 bits, which
is the underlying mathematical model behind the two's complement representation.
### Provide rotate operators
We could provide bitwise rotation operators. However, there doesn't seem to be a
sufficient need to justify adding another operator symbol for this purpose.
### Guarantee behavior of large shifts
Logically, the behavior of shifts is meaningful for all values of the second
operand:
- A shift by an amount greater than or equal to the bit-width of the first
operand will shift out all of the original bits, producing a result where
all value bits are the same.
- A shift in one direction by a negative amount is treated as a shift in the
opposite direction by the negation of that amount.
Put another way, we can view the bits of the first operand as an N-bit window
into an infinite sequence of bits, with infinitely many leading sign bits (all
zeroes for an unsigned value) and infinitely many trailing zero bits after a
notional binary point, and a shift moves that window around. Or equivalently, a
shift is always a multiplication by 2<sup>N</sup> followed by rounding and
wrapping.
We could provide the correct result for all shifts, regardless of the magnitude
of the second operand. This is the approach taken by Python, except that Python
rejects negative shift counts. The primary reason we do not do this is lack of
hardware support. For example, x86 does not have an instruction to perform this
operation. Rather, x86 provides shift instructions that mask off all bits of the
second operand except for the bottom 5 or 6, meaning that a left shift of a
64-bit operand by 64 will return the operand unchanged rather than producing
zero.
We could instead provide x86-like behavior, guaranteeing to consider only the
lowest `N` bits of the second operand when the first operand is an `iN` or `uN`.
This is the approach taken by Java for its 32-bit `int` type and 64-bit `long`
type, where the second operand is taken modulo 32 or 64, respectively, and in
JavaScript, where operands of bitwise and bit-shift operators are treated as
32-bit integers and the second operand of a shift is taken modulo 32. This
approach would provide an operation that can be implemented by a single
instruction on x86 platforms when `N` is 32 or 64, and for all smaller types and
for all other platforms the operation can be implemented with two instructions:
a mask and a shift. For larger types, single-instruction support may not be
available, but nonetheless the performance will be close to optimal, requiring
at most one additional mask. There is still some performance cost in some cases,
but the primary reason we do not do this is the same reason we choose to not
define signed integer overflow: this masked result is unlikely to be the value
that the developer actually wanted.
Instead of the above options, Carbon treats a second operand that is not in the
interval [0, N) as a programming error, just like signed integer overflow:
- Debugging builds can detect and report this error without the risk of false
positives.
- Performance builds can optimize on the basis that this situation will not
occur, and can in particular use the dedicated x86 instructions that ignore
the high order bits of the second operand.
- Optimized builds guarantee that either the programming error results in
program termination or that _some_ value is produced, and moreover that said
value is the result of applying _some_ mathematical shift to the input. For
example, it's valid for an `i32` shift to be implemented by an x86 64-bit
shift that will produce 0 if the second operand is in [32, 63) but that will
treat a second operand of, say, 64 or -64 the same as 0.
### Support shifting a constant by a variable
We considered various ways to support
```
var a: i32 = ...;
var b: i32 = 1 << a;
var c: i32 = 1234 >> a;
```
with no explicit type specified for the first operand of a bit-shift operator.
We considered the following options:
- Use the type of the second operand as the result type. This would be
surprising, because the type of the second operand doesn't otherwise
influence the result type of a built-in bit-shift operator.
- Use some suitable integer type that can fit the first operand. However, this
is unlikely to do the right thing for a left-shift, and will frequently pick
either a type that's too large, resulting in the program being rejected due
to narrowing, or a type that's too small, resulting in a program that has
undefined behavior due to the second operand being too large. We could apply
this approach only for right shifts, but it was deemed too inconsistent to
use different rules for left and right shifts.
- We could find a way to defer picking the type in which the operation is
performed until later. For example, we could treat `1 << a` as a value of a
new type that carries its left-hand operand as a type parameter and its
right-hand operand as runtime state, and allow that type to be converted in
the same way as its integer constant. However, this would introduce
substantial complexity: reasonable and expected uses such as
```
var mask: u32 = (1 << a) - 1;
```
would require a second new type for a shifted value plus an offset, and
general support would require a facility analogous to
[expression templates](https://en.wikipedia.org/wiki/Expression_templates).
Further, this facility would allow implicit conversions that notionally
overflow, such as would happen in the above example when `a` is greater
than 32.
In the absence of a good approach, we disallow such conversions for now. The
above example can be written as:
```
var a: i32 = ...;
var b: i32 = (1 as i32) << a;
var c: i32 = (1234 as i32) >> a;
```
### Converting complements to unsigned types
We view an integer constant has having infinitely many high-order sign bits
followed by some number of lower-order value bits. As a consequence, the
complement of a positive integer constant is negative. As a result, some
important forms of initialization use a negative integer constant initializer
for an unsigned type:
```
// Initializer here is the integer value -8.
var mask: u32 = ^7;
```
We considered some options for handling this:
- We could allow negative integer constants to convert to unsigned types if
doing so only discards sign bits. This violates the "semantics-preserving"
rule for implicit conversions.
- We could change our model of integer constants to distinguish between
"masks" -- numbers with infinitely many 1 bits preceding the value bits that
are nonetheless not considered to be negative. This was considered to
introduce too much complexity.
- We could allow conversions to unsigned types from signed types and negative
constants in general, or at least in cases where the signed operand is no
wider than the unsigned type, and perform wrapping. The latter option seems
plausible, but we don't have sufficient motivation for it, and were worried
about a risk of bugs from allowing an implicit conversion at runtime that
converts a negative value to an unsigned type.
- We could reject such initializations, with an explicit conversion required
to convert such values to unsigned types. This seems to present unacceptable
ergonomics for code performing bit-manipulation.
On balance, our preferred option was to permit implicit conversions from
negative literals to unsigned types so long as we only discard sign bits.