mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-10-04 18:11:05 +01:00
This PR corrects misspellings identified by the [check-spelling action](https://github.com/marketplace/actions/check-spelling). The misspellings have been reported at https://github.com/jsoref/carbon-lang/commit/38a1c1640151899fd6da0442a92557f9543b6280#commitcomment-79197316 The action reports that the changes in this PR would make it happy: https://github.com/jsoref/carbon-lang/commit/173c8f9083a68aa61f7cfe94f720f1e5dc7f1ea3 Note: this PR does not include the action. If you're interested in running a spell check on every PR and push, that can be offered separately. Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
513 lines
20 KiB
Markdown
513 lines
20 KiB
Markdown
# Comparison 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/702)
|
|
|
|
<!-- toc -->
|
|
|
|
## Table of contents
|
|
|
|
- [Problem](#problem)
|
|
- [Background](#background)
|
|
- [Terminology](#terminology)
|
|
- [Usage in existing languages](#usage-in-existing-languages)
|
|
- [Three-way comparisons](#three-way-comparisons)
|
|
- [Chained comparisons](#chained-comparisons)
|
|
- [Proposal](#proposal)
|
|
- [Details](#details)
|
|
- [Precedence](#precedence)
|
|
- [Associativity](#associativity)
|
|
- [Built-in comparisons and implicit conversions](#built-in-comparisons-and-implicit-conversions)
|
|
- [Consistency with implicit conversions](#consistency-with-implicit-conversions)
|
|
- [Comparisons with constants](#comparisons-with-constants)
|
|
- [Performance](#performance)
|
|
- [Overloading](#overloading)
|
|
- [Default implementations for basic types](#default-implementations-for-basic-types)
|
|
- [Rationale based on Carbon's goals](#rationale-based-on-carbons-goals)
|
|
- [Alternatives considered](#alternatives-considered)
|
|
- [Alternative symbols](#alternative-symbols)
|
|
- [Chained comparisons](#chained-comparisons-1)
|
|
- [Convert operands like C++](#convert-operands-like-c)
|
|
- [Provide a three-way comparison operator](#provide-a-three-way-comparison-operator)
|
|
- [Allow comparisons as the operand of `not`](#allow-comparisons-as-the-operand-of-not)
|
|
|
|
<!-- tocstop -->
|
|
|
|
## Problem
|
|
|
|
We need to be able to compare values for equality, and to compare ordered values
|
|
for relative ordering.
|
|
|
|
## Background
|
|
|
|
### Terminology
|
|
|
|
We refer to tests that check whether two values are the same or different as
|
|
_equality_ comparisons, and to tests that determine the relative ordering of two
|
|
values as _relational_ comparisons.
|
|
|
|
### Usage in existing languages
|
|
|
|
There is near-universal convention on the use of the following symbols for
|
|
relational operators:
|
|
|
|
- `<`, `<=`, `>`, and `>=` perform ordered comparisons (less than, less than
|
|
or equal to, greater than, greater than or equal to).
|
|
|
|
There are rare exceptions in somewhat esoteric languages: some languages use `≤`
|
|
and `≥`, but these are not straightforward to type for many potential Carbon
|
|
developers.
|
|
|
|
For equality operators, there is some divergence but still a very strong trend:
|
|
|
|
- C-family languages, Rust, Swift, Kotlin, Zig, Nim, Ruby, etc. use `==` for
|
|
equality comparison and `!=` for inequality comparison.
|
|
- Some languages, such as ALGOL, APL, BASIC, and PL/I, use `=` as equality
|
|
comparison, with some using a different symbol (such as `:=` or `<-`) for
|
|
assignment and others distinguishing assignment from equality comparison
|
|
based on context.
|
|
- Haskell and Fortran use `==` for "equal to" and `/=` for "not equal to". The
|
|
latter is intended to resemble a ≠ symbol.
|
|
- Some languages, such as Pascal and BASIC, use `<>` for inequality
|
|
comparison. Python 2 permits this as a synonym for `!=`.
|
|
- Perl uses `eq` and `ne` for string comparisons; some shells and UNIX `test`
|
|
use `-eq` and `-ne` for integer comparisons.
|
|
|
|
Some languages support multiple different kinds of equality comparison, such as
|
|
both a value comparison (typically `==`) and an object identity comparison
|
|
(typically `===` or `is`). Some languages that freely convert between numbers
|
|
and strings have different operators to perform a string comparison versus a
|
|
numeric comparison. Fortran has custom `.eqv.` and `.neqv.` for equality
|
|
comparisons of Boolean values.
|
|
|
|
Some languages have synonyms for equality operators. For example, Fortran allows
|
|
`.eq.`, `.ne.`, `.gt.`, and so on, as synonyms for `==`, `/=`, `>`, and so on.
|
|
This appears to be historical: FORTRAN 77 had only the dotted forms of these
|
|
operators.
|
|
|
|
### Three-way comparisons
|
|
|
|
C++ has three-way comparisons, written using the `<=>` operator. These provide a
|
|
useful mechanism to allow overloading the behavior of relational comparisons
|
|
without defining four separate operator overloads for relational comparisons.
|
|
|
|
Similarly, Python provides a `__cmp__` special method that can be used to
|
|
implement all equality and relational comparisons.
|
|
|
|
### Chained comparisons
|
|
|
|
Python permits comparisons to be chained: that is, `a < b <= c` is interpreted
|
|
as `a < b and b <= c`, except that `b` is evaluated only once. In most C-family
|
|
languages, that expression is instead interpreted as `(a < b) <= c`, which
|
|
computes the value of `a < b`, maps `false` to `0` and `true` to `1`, then
|
|
compares the result to `c`.
|
|
|
|
## Proposal
|
|
|
|
Carbon will provide the following operators:
|
|
|
|
- Equality comparison operators: `==` and `!=`.
|
|
- Relational comparison operators: `<`, `<=`, `>`, `>=`.
|
|
|
|
Each has the obvious mathematical meaning, where `==` means =, `!=` means ≠,
|
|
`<=` means ≤, and `>=` means ≥.
|
|
|
|
There will be no three-way comparison operator symbol. The interface used to
|
|
support overloading comparison operators will provide a named function to
|
|
perform three-way comparisons.
|
|
|
|
Chained comparisons are an error: a comparison expression cannot appear as an
|
|
unparenthesized operand of another comparison operator.
|
|
|
|
For built-in types, we will follow these rules:
|
|
|
|
- Behave consistently with implicit conversions: if an operation is valid
|
|
between built-in types `T` and `U`, then it is valid between built-in types
|
|
that implicitly convert to `T` and `U`.
|
|
- Never invent an intermediate type that is larger than both operand types.
|
|
- A comparison produces either a mathematically correct answer or a
|
|
compilation error.
|
|
|
|
The first two rules are expected to also apply for other built-in operators,
|
|
such as arithmetic. The third rule is specific to comparisons.
|
|
|
|
One consequence of the first rule is that we do not convert operands in a way
|
|
that might lose information. This is generally also implied by the third rule.
|
|
|
|
## Details
|
|
|
|
All six operators are infix binary operators. For standard Carbon types, they
|
|
produce a `Bool` value.
|
|
|
|
### Precedence
|
|
|
|
The comparison operators are all at the same precedence level. This level is
|
|
lower than operators used to compute (non-Boolean) values, higher than the
|
|
logical operators `and` and `or`, and incomparable with the precedence of `not`.
|
|
|
|
For example, this is OK:
|
|
|
|
```
|
|
if (n + m * 3 < n * n and 3 < m and m < 6) {}
|
|
```
|
|
|
|
... but these are errors:
|
|
|
|
```
|
|
// Error, ambiguous: `(not a) == b` or `not (a == b)`?
|
|
if (not a == b) {}
|
|
// Error, requires parentheses: `a == (not b)`.
|
|
if (a == not b) {}
|
|
// Error, requires parentheses: `not (f < 5.0)`.
|
|
if (not f < 5.0) {}
|
|
```
|
|
|
|
### Associativity
|
|
|
|
The comparison operators are non-associative. For example:
|
|
|
|
```
|
|
// Error, need `3 < m and m < 6`.
|
|
if (3 < m < 6) {}
|
|
// Error, need `a == b and b == c`.
|
|
if (a == b == c) {}
|
|
// Error, need `(m > 1) == (n > 1)`.
|
|
if (m > 1 == n > 1) {}
|
|
```
|
|
|
|
### Built-in comparisons and implicit conversions
|
|
|
|
Built-in comparisons are permitted:
|
|
|
|
- when both operands are of standard Carbon integer types (`Int(n)` or
|
|
`Unsigned(n)`), or
|
|
- when both operands are of standard Carbon floating-point types (`Float(n)`),
|
|
or
|
|
- when one operand is of floating-point type and the other is of integer type,
|
|
if all values of the integer type can be exactly represented in the
|
|
floating-point type
|
|
|
|
In each case, the result is the mathematically-correct answer. This applies even
|
|
when comparing `Int(n)` with `Unsigned(m)`.
|
|
|
|
For example:
|
|
|
|
```
|
|
// The value of `v` is True, because `a` is less than `b`, even though the
|
|
// result of either an `i32` comparison or a `u32` comparison would be False.
|
|
fn f(a: i32, b: u32) -> Bool { return a < b; }
|
|
let v: Bool = f(-1, 4_000_000_000);
|
|
|
|
// This does not compile, because `i64` values in general (and 10^18 in
|
|
// particular) are not exactly representable in the type `f32`.
|
|
let f: f32 = 1.0e18;
|
|
let n: i64 = 1_000_000_000_000_000_000;
|
|
let w: Bool = f == n;
|
|
```
|
|
|
|
Comparisons involving integer and floating-point constants are not covered by
|
|
these rules and are [discussed separately](#comparisons-with-constants).
|
|
|
|
#### Consistency with implicit conversions
|
|
|
|
As specified in [#820](https://github.com/carbon-language/carbon-lang/pull/820),
|
|
we support the following implicit conversions:
|
|
|
|
- from `Int(n)` to `Int(m)` if `m > n`,
|
|
- from `Unsigned(n)` to `Int(m)` or `Unsigned(m)` if `m > n`,
|
|
- from `Float(n)` to `Float(m)` if `m > n`, and
|
|
- from `Int(n)` to `Float(m)` if `Float(m)` can represent all values of
|
|
`Int(n)`.
|
|
|
|
These rules can be summarized as: a type `T` can be converted to `U` if every
|
|
value of type `T` is a value of type `U`.
|
|
|
|
Additionally [#820](https://github.com/carbon-language/carbon-lang/pull/820)
|
|
permits conversions from certain kinds of integer and floating-point constants
|
|
to `Int(n)` and `Float(n)` types if the constant can be represented in the type.
|
|
|
|
All built-in comparisons can be viewed as performing implicit conversions on at
|
|
most one of the operands in order to reach a suitable pair of identical or very
|
|
similar types, and then performing a comparison on those types. The target types
|
|
for these implicit conversions are, for each suitable value `n`:
|
|
|
|
- `Int(n)` vs `Int(n)`
|
|
- `Unsigned(n)` vs `Unsigned(n)`
|
|
- `Int(n)` vs `Unsigned(n)`
|
|
- `Unsigned(n)` vs `Int(n)`
|
|
- `Float(n)` vs `Float(n)`
|
|
|
|
There will in general be multiple combinations of implicit conversions that will
|
|
lead to one of the above forms, but we will arrive at the same result regardless
|
|
of which is selected, because all comparisons are mathematically correct and all
|
|
implicit conversions are lossless. Implementations are expected to do whatever
|
|
is most efficient: for example, for `u16 < i32` it is likely that the best
|
|
choice would be to promote the `u16` to `i32`, not `u32`.
|
|
|
|
Because we only ever convert at most one operand, we never use an intermediate
|
|
type that is larger than both input types. For example, both `i32` and `f32` can
|
|
be implicitly converted to `f64`, but we do not permit comparisons between `i32`
|
|
and `f32` even though we could perform those comparisons in `f64`. If such
|
|
comparisons were permitted, the results could be surprising:
|
|
|
|
```
|
|
// OK, i32 can exactly represent this value.
|
|
var n: i32 = 2_000_000_001;
|
|
// OK, this value is within the representable range for f32.
|
|
var f: f32 = 2_000_000_001.0;
|
|
// This comparison could compare unequal, because f32 cannot exactly represent
|
|
// the value 2,000,000,001.
|
|
if (n == f) { ... }
|
|
// OK with explicit cast, but may still compare unequal.
|
|
if (n == f as f64) { ... }
|
|
if (n as f64 == f) { ... }
|
|
```
|
|
|
|
The two kinds of mixed-type comparison may be [less efficient](#performance)
|
|
than the other kinds due to the slightly wider domain.
|
|
|
|
Note that this approach diverges from C++, which would convert both operands to
|
|
a common type first, sometimes performing a lossy conversion potentially giving
|
|
an incorrect result, sometimes converting both operands, and sometimes using a
|
|
wider type than either of the operand types.
|
|
|
|
#### Comparisons with constants
|
|
|
|
As described in [#820](https://github.com/carbon-language/carbon-lang/pull/820),
|
|
integer constants can be implicitly converted to any integer or floating-point
|
|
type that can represent their value, and floating-point constants can be
|
|
implicitly converted to any floating-point type that can represent their value.
|
|
We permit the following comparisons involving constants:
|
|
|
|
- A constant can be compared with a value of any type to which it can be
|
|
implicitly converted.
|
|
- Any two constants can be compared, even if there is no type that can
|
|
represent both.
|
|
|
|
Note that this disallows comparisons between, for example, `i32` and an integer
|
|
literal that cannot be represented in `i32`. Such comparisons would always be
|
|
tautological. This decision should be revisited if it proves problematic in
|
|
practice, for example in templated code where the literal is sometimes in range.
|
|
|
|
#### Performance
|
|
|
|
The choice to give correct results for signed/unsigned comparisons has a
|
|
performance impact in practice, because it exposes operations that some
|
|
processors do not currently directly support.
|
|
[Sample microbenchmarks](https://quick-bench.com/q/1_xA8G_jXci_yeOKt0WgCc6eGN4)
|
|
for implementations of several operations show the following performance on
|
|
x86_64:
|
|
|
|
| Operation | Mathematical comparison time | C++ comparison time | Ratio |
|
|
| ----------- | ---------------------------- | ------------------- | ----- |
|
|
| `i64 < u64` | 1636 | 798 | 2.0x |
|
|
| `u64 < i64` | 1956 | 798 | 2.5x |
|
|
|
|
The execution times here are computed as operation time minus no-op time.
|
|
|
|
The mixed-type operations typically have 2-2.5x the execution time of the
|
|
same-type operations. However, this is a predictable performance change, and can
|
|
be controlled by the developer by converting the operands to a suitable type
|
|
prior to the conversion if a faster same-type comparison is preferred over a
|
|
correct mixed-type comparison.
|
|
|
|
The above comparison attempts to demonstrate a worst-case difference. In many
|
|
cases, better code can be generated for the mixed-type comparison. For example,
|
|
when
|
|
[branching on the result of the comparison](https://quick-bench.com/q/mXJiHK3_RcCH4fgB88phQscLu88),
|
|
the difference is significantly reduced:
|
|
|
|
| Operation | Mathematical comparison time | C++ comparison time | Ratio |
|
|
| ----------- | ---------------------------- | ------------------- | ----- |
|
|
| `i64 < u64` | 996 | 991 | 1.0x |
|
|
| `u64 < i64` | 1973 | 997 | 2.0x |
|
|
|
|
### Overloading
|
|
|
|
Separate interfaces will be provided to permit overloading equality and
|
|
relational comparisons. The exact design of those interfaces is left to a future
|
|
proposal. As non-binding design guidance for such a proposal:
|
|
|
|
- The interface for equality comparisons should primarily provide the ability
|
|
to override the behavior of `==`. The `!=` operator can optionally also be
|
|
overridden, with a default implementation that returns `not (a == b)`.
|
|
Overriding `!=` separately from `==` is expected to be used to support
|
|
floating-point NaN comparisons and for C++ interoperability.
|
|
- The interface for relational comparisons should primarily provide the
|
|
ability to specify a three-way comparison operator. The individual
|
|
relational comparison operators can optionally be overridden separately,
|
|
with a default implementation in terms of the three-way comparison operator.
|
|
This facility is expected to be used primarily to support C++
|
|
interoperability.
|
|
- Overloaded comparison operators may wish to produce a type other than
|
|
`Bool`, for uses such as a vector comparison producing a vector of `Bool`
|
|
values. We should decide whether we wish to support such uses.
|
|
|
|
### Default implementations for basic types
|
|
|
|
In addition to being defined for standard Carbon numeric types, equality and
|
|
relational comparisons are also defined for all "data" types:
|
|
|
|
- Tuples.
|
|
- Structs (structural data classes).
|
|
- Classes implementing an interface that identifies them as data classes.
|
|
|
|
Relational comparisons for these types provide a lexicographical ordering. This
|
|
proposal defers to
|
|
[#710](https://github.com/carbon-language/carbon-lang/issues/710) for details on
|
|
comparison support for classes.
|
|
|
|
In each case, the comparison is only available if it is supported by all element
|
|
types.
|
|
|
|
The `Bool` type should be treated as a choice type, and so should support
|
|
equality comparisons and relational comparisons if and only if choice types do
|
|
in general. That decision is left to a future proposal.
|
|
|
|
## Rationale based on Carbon's goals
|
|
|
|
- _Performance-critical software:_
|
|
|
|
- The use of a three-way comparison as the central primitive for
|
|
overloading relational comparisons provides predictable, composable
|
|
performance for comparing hierarchical data structures.
|
|
- The performance of mixed comparisons may be slower than in C++, but this
|
|
is because it's performing a different operation. This performance
|
|
change is predictable, and can be controlled by the programmer by
|
|
performing suitable non-value-preserving casts to a common type prior to
|
|
the comparison.
|
|
|
|
- _Code that is easy to read, understand, and write:_
|
|
|
|
- The chosen precedence and associativity rules aim to avoid bugs and
|
|
ensure the code does what it appears to do, requiring parentheses in
|
|
cases where the intent is unclear.
|
|
- The choice to not perform lossy conversions on operands of a comparison
|
|
operator removes a source of bugs caused by unintended lossy
|
|
conversions.
|
|
|
|
- _Interoperability with and migration from existing C++ code:_
|
|
|
|
- The use of the chosen operator symbols exactly matches C++, reducing
|
|
friction for developers and code moving between the two languages, and
|
|
for interoperability.
|
|
|
|
## Alternatives considered
|
|
|
|
### Alternative symbols
|
|
|
|
We could use `/=` instead of `!=` for not-equal comparisons.
|
|
|
|
Advantages:
|
|
|
|
- Avoids overloading `!` for both "not equals" and template/generic use in
|
|
`:!` bindings.
|
|
- There is no other usage of `!` meaning "not" in the language because we use
|
|
a `not` operator.
|
|
|
|
Disadvantages:
|
|
|
|
- Unfamiliar to C++ programmers.
|
|
- `a /= b` would likely be expected to mean an `a = a / b` compound
|
|
assignment.
|
|
- Breaks consistency with Python, which uses `not` for logical negation and
|
|
`!=` for inequality comparison.
|
|
|
|
We could use `=/=` instead of `!=` for not-equal comparisons.
|
|
|
|
Advantages:
|
|
|
|
- As above; also `=/=` looks like an `==` with a line through the middle.
|
|
|
|
Disadvantages:
|
|
|
|
- This would be inventive and unlike all other languages. As above, breaks
|
|
consistency with Python.
|
|
- This would make `=/=` one character longer, and harder to type on US-ASCII
|
|
keyboards because the keys are distant but likely to be typed with the same
|
|
finger.
|
|
|
|
### Chained comparisons
|
|
|
|
We could support Python-like chained comparisons.
|
|
|
|
Advantages:
|
|
|
|
- Small ergonomic improvement for range comparisons.
|
|
- Middle operand is evaluated only once.
|
|
|
|
Disadvantages:
|
|
|
|
- Using the middle expression as an argument to two different functions may
|
|
create problems, as the value will need to be stored somewhere, potentially
|
|
changing the semantics of the operator expression as we can no longer move
|
|
from the operand.
|
|
- Both short-circuiting behavior and non-short-circuiting behavior will be
|
|
surprising and unintuitive to some. The short-circuiting option will
|
|
introduce control flow without a keyword to announce it, which goes against
|
|
our design decision to use a keyword for `and` and `or` to announce the
|
|
control flow. The non-short-circuiting option will evaluate subexpressions
|
|
unnecessarily, which creates a tension with our performance goal.
|
|
- Experienced C++ developers may expect a different behavior, such as
|
|
`a < b == cmp` comparing the result of `a < b` against the Boolean value
|
|
`cmp`.
|
|
|
|
See also the ongoing discussion in
|
|
[#451](https://github.com/carbon-language/carbon-lang/issues/451).
|
|
|
|
### Convert operands like C++
|
|
|
|
We could convert the operands of comparison operators in a way that's equivalent
|
|
to C++'s behavior.
|
|
|
|
Advantages:
|
|
|
|
- May ease migration from C++.
|
|
- May allow programmers to reuse some intuition, for example when comparing
|
|
floating-point values against integer values.
|
|
- Allows more efficient machine code to be generated for source code that
|
|
takes no special care about the types of comparison operands.
|
|
- Improves performance predictability for C++ developers unfamiliar with
|
|
Carbon's rules.
|
|
|
|
Disadvantages:
|
|
|
|
- Produces incorrect results.
|
|
- Does not provide a simple syntax for correct mixed-type comparisons.
|
|
|
|
### Provide a three-way comparison operator
|
|
|
|
We could provide a symbol for three-way comparisons, such as C++20's `<=>`.
|
|
|
|
Advantages:
|
|
|
|
- The use of a symbol rather than a named member of an interface for this
|
|
functionality may ease migration from C++20.
|
|
|
|
Disadvantages:
|
|
|
|
- Reserves a symbol for an operation that should not be used directly except
|
|
in special circumstances, and that will produce a nuanced type even when
|
|
comparing standard Carbon types such as `f32`.
|
|
|
|
### Allow comparisons as the operand of `not`
|
|
|
|
We could permit comparisons to appear as the immediate operand of `not` without
|
|
parentheses.
|
|
|
|
Advantages:
|
|
|
|
- Provides an easier syntax for floating-point comparisons where the desired
|
|
result for a NaN operand is `True` rather than `False`: `not f < 5.0`.
|
|
|
|
Disadvantages:
|
|
|
|
- Introduces ambiguity when comparing Boolean values: `not cond1 == cond2`
|
|
might intend to compare `not cond1` to `cond2` rather than comparing
|
|
`cond1 != cond2`.
|