From df3df9b48c0c0852401aacbcc0d58b0e260eb925 Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Tue, 3 Aug 2021 17:31:40 -0700 Subject: [PATCH 01/16] Comparison operators. This proposal introduces the operators `==`, `!=`, `<`, `<=`, `>`, and `>=` to Carbon. --- proposals/README.md | 1 + proposals/p0702.md | 356 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 357 insertions(+) create mode 100644 proposals/p0702.md diff --git a/proposals/README.md b/proposals/README.md index 6cb59110b832e..b03f977997cc9 100644 --- a/proposals/README.md +++ b/proposals/README.md @@ -64,5 +64,6 @@ request: - [0623 - Require braces](p0623.md) - [0646 - Low context-sensitivity principle](p0646.md) - [0680 - And, or, not](p0680.md) +- [0702 - Comparison operators](p0702.md) diff --git a/proposals/p0702.md b/proposals/p0702.md new file mode 100644 index 0000000000000..42d8172c03372 --- /dev/null +++ b/proposals/p0702.md @@ -0,0 +1,356 @@ +# Comparison operators + + + +[Pull request](https://github.com/carbon-language/carbon-lang/pull/702) + + + +## 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) + - [Conversions](#conversions) + - [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) + - [Disallow relational comparisons of Boolean values](#disallow-relational-comparisons-of-boolean-values) + + + +## 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 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. + +## 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: + +``` +if (not a == b) { // error, ambiguous: `(not a) == b` or `not (a == b)`? +if (a == not b) { // error, requires parentheses: `a == (not b)` +if (not f < 5.0) { // error, requires parentheses: `not (f < 5.0)` +``` + +### Associativity + +The comparison operators are non-associative. For example: + +``` +if (3 < m < 6) { // error, need `3 < m and m < 6` +if (a == b == c) { // error, need `a == b and b == c` +if (m > 1 == n > 1) { // error, need `(m > 1) == (n > 1)` +``` + +### Conversions + +When both operands are of standard Carbon numeric types (`Int(n)` or +`Float(n)`), no conversions are performed on either operand, and the result is +the mathematically correct result for that comparison, or `False` if either +operand is a NaN. 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); + +// The value of `w` is False, because `f` has value 999999984306749440, which +// is not exactly equal to n. +let f: f32 = 1.0e18; +let n: i64 = 1_000_000_000_000_000_000; +let w: Bool = f == n; +``` + +An equivalent viewpoint is that the comparison is performed in a hypothetical +suffiicently large type. For example, a comparison of `i32` against `u32` can be +performed in `i64`, and a comparison of `f32` against `i32` can be performed in +`f64`. However, no such type is required to actually exist. + +Note that this diverges from C++, which would convert both operands to a common +type first, sometimes performing a lossy conversion. + +### 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)`. +- 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. +- 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. + +### Default implementations for basic types + +In addition to being defined for standard Carbon numeric types, equality +comparisons are also defined for all "data" types: + +- Tuples. +- Structs (structural data classes). +- Classes implementing an interface that identifies them as data classes. + +In addition, relational comparisons are defined for tuples, and provide a +lexicographical ordering. + +In each case, the ordering is only available if it is supported by all element +types. + +The `Bool` type supports equality comparisons and relational comparisons. For +relational comparisons, `False` is treated as being less than `True`. + +## 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. + +- _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 convert 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. + +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. +- 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. + +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. + +### 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. +- May allow more efficient machine code to be generated for source code that + takes no special care about the types of comparison operands. + +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 cmoparing + `cond1 != cond2`. + +### Disallow relational comparisons of Boolean values + +We could disallow ordered comparisons of Boolean values. + +Advantages: + +- Disallows an operation that might be unintended. + +Disadvantages: + +- Disallows an operation that might be intended. +- Likely to make `Bool` behave differently from discriminated union types, + which are likely to be treated as data types. From 8e0e20585604f15b30dd631ca852f275ea6bf405 Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Tue, 3 Aug 2021 19:12:04 -0700 Subject: [PATCH 02/16] Avoid trailing comments. --- proposals/p0702.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/proposals/p0702.md b/proposals/p0702.md index 42d8172c03372..23547be87f48e 100644 --- a/proposals/p0702.md +++ b/proposals/p0702.md @@ -136,15 +136,18 @@ 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) { +if (n + m * 3 < n * n and 3 < m and m < 6) {} ``` ... but these are errors: ``` -if (not a == b) { // error, ambiguous: `(not a) == b` or `not (a == b)`? -if (a == not b) { // error, requires parentheses: `a == (not b)` -if (not f < 5.0) { // error, requires parentheses: `not (f < 5.0)` +// 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 @@ -152,9 +155,12 @@ if (not f < 5.0) { // error, requires parentheses: `not (f < 5.0)` The comparison operators are non-associative. For example: ``` -if (3 < m < 6) { // error, need `3 < m and m < 6` -if (a == b == c) { // error, need `a == b and b == c` -if (m > 1 == n > 1) { // error, need `(m > 1) == (n > 1)` +// 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) {} ``` ### Conversions From ee102de921b60f15a1f8c0a6de7c677523a9f37a Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Tue, 3 Aug 2021 19:13:01 -0700 Subject: [PATCH 03/16] Fix broken formatting. --- proposals/p0702.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/proposals/p0702.md b/proposals/p0702.md index 23547be87f48e..ccede632eb5b9 100644 --- a/proposals/p0702.md +++ b/proposals/p0702.md @@ -198,9 +198,8 @@ 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)`. + to override the behavior of `==`. The `!=` operator can optionally also be + overridden, with a default implementation that returns `not (a == b)`. - 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, From df34d0b78ac4e6c4dc2bd5cf6d3a00dd275827fd Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Wed, 4 Aug 2021 15:59:41 -0700 Subject: [PATCH 04/16] Apply suggestions from code review Co-authored-by: Jon Meow <46229924+jonmeow@users.noreply.github.com> --- proposals/p0702.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proposals/p0702.md b/proposals/p0702.md index ccede632eb5b9..508b5511c23ca 100644 --- a/proposals/p0702.md +++ b/proposals/p0702.md @@ -184,7 +184,7 @@ let w: Bool = f == n; ``` An equivalent viewpoint is that the comparison is performed in a hypothetical -suffiicently large type. For example, a comparison of `i32` against `u32` can be +sufficiently large type. For example, a comparison of `i32` against `u32` can be performed in `i64`, and a comparison of `f32` against `i32` can be performed in `f64`. However, no such type is required to actually exist. @@ -343,7 +343,7 @@ Advantages: Disadvantages: - Introduces ambiguity when comparing Boolean values: `not cond1 == cond2` - might intend to compare `not cond1` to `cond2` rather than cmoparing + might intend to compare `not cond1` to `cond2` rather than comparing `cond1 != cond2`. ### Disallow relational comparisons of Boolean values From 637db24df41b76cda7bca0546641cc61562d6965 Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Wed, 4 Aug 2021 15:59:23 -0700 Subject: [PATCH 05/16] Addressing review comments. --- proposals/p0702.md | 63 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/proposals/p0702.md b/proposals/p0702.md index 508b5511c23ca..29de014025130 100644 --- a/proposals/p0702.md +++ b/proposals/p0702.md @@ -23,6 +23,7 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - [Precedence](#precedence) - [Associativity](#associativity) - [Conversions](#conversions) + - [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) @@ -191,6 +192,32 @@ performed in `i64`, and a comparison of `f32` against `i32` can be performed in Note that this diverges from C++, which would convert both operands to a common type first, sometimes performing a lossy conversion. +#### Performance + +The choice to not convert has a performance impact in practice, because it +exposes operations that some processors do not currently directly support. +[Sample microbenchmarks](https://godbolt.org/z/dfGe4MhEx) for implementations of +several operations show the following performance on x86_64 (use the Quick-bench +link in Compiler Explorer to run the benchmarks): + +| Operation | Mathematical comparison time | C++ comparison time | Ratio | +| --------------- | ---------------------------- | ------------------- | ----- | +| `i64 < u64` | 2814 | 992 | 2.8x | +| `u64 < i64` | 1957 | 1012 | 1.9x | +| `f64 == i64` | 4996 | 2197 | 2.3x | +| `f64 < i64` (a) | 2012 | 2841 | 0.7x | +| `f64 < i64` (b) | 5332 | 2647 | 2.0x | + +The mathematical code sequence used for `f64 < i64` introduces a branch around a +slow path; in the benchmark, that branch should never be mispredicted. Line (a) +demonstrates the fast path and line (b) demonstrates the slow path. + +The mixed-type operations are typically 2-3x slower than 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. + ### Overloading Separate interfaces will be provided to permit overloading equality and @@ -210,17 +237,19 @@ proposal. As non-binding design guidance for such a proposal: ### Default implementations for basic types -In addition to being defined for standard Carbon numeric types, equality -comparisons are also defined for all "data" 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. -In addition, relational comparisons are defined for tuples, and provide a -lexicographical ordering. +Relational comparisons for these types provide a lexicographical ordering. This +proposal defers to +[#561](https://github.com/carbon-language/carbon-lang/pull/561) for details on +comparison support for classes. -In each case, the ordering is only available if it is supported by all element +In each case, the comparison is only available if it is supported by all element types. The `Bool` type supports equality comparisons and relational comparisons. For @@ -266,6 +295,8 @@ 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. @@ -275,7 +306,8 @@ Advantages: Disadvantages: -- This would be inventive and unlike all other languages. +- 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. @@ -287,6 +319,7 @@ We could support Python-like chained comparisons. Advantages: - Small ergonomic improvement for range comparisons. +- Middle operand is evaluated only once. Disadvantages: @@ -295,7 +328,17 @@ Disadvantages: 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. + surprising and unintuitive to some. The short-circiuting 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++ @@ -307,8 +350,10 @@ Advantages: - May ease migration from C++. - May allow programmers to reuse some intuition, for example when comparing floating-point values against integer values. -- May allow more efficient machine code to be generated for source code that +- 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: @@ -353,6 +398,8 @@ We could disallow ordered comparisons of Boolean values. Advantages: - Disallows an operation that might be unintended. +- Relational Boolean comparisons are unusual and liable to result in a + readability problem everywhere they appear. Disadvantages: From f9a6a0669952a5ba20fd87a931f87b63a16e694e Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Wed, 4 Aug 2021 16:04:21 -0700 Subject: [PATCH 06/16] Update proposals/p0702.md Co-authored-by: Geoff Romer --- proposals/p0702.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/p0702.md b/proposals/p0702.md index 29de014025130..75c77b5e8a975 100644 --- a/proposals/p0702.md +++ b/proposals/p0702.md @@ -166,7 +166,7 @@ if (m > 1 == n > 1) {} ### Conversions -When both operands are of standard Carbon numeric types (`Int(n)` or +When both operands are of standard Carbon numeric types (`Int(n)`, `Unsigned(n)`, or `Float(n)`), no conversions are performed on either operand, and the result is the mathematically correct result for that comparison, or `False` if either operand is a NaN. For example: From 408c392d2fcdeac0b31d09729a05030c79b85c84 Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Wed, 4 Aug 2021 16:07:04 -0700 Subject: [PATCH 07/16] Defer permission for comparison of `Bool` values to choice type proposal. --- proposals/p0702.md | 30 +++++++----------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/proposals/p0702.md b/proposals/p0702.md index 75c77b5e8a975..f864e2ef92135 100644 --- a/proposals/p0702.md +++ b/proposals/p0702.md @@ -33,7 +33,6 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - [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) - - [Disallow relational comparisons of Boolean values](#disallow-relational-comparisons-of-boolean-values) @@ -166,10 +165,10 @@ if (m > 1 == n > 1) {} ### Conversions -When both operands are of standard Carbon numeric types (`Int(n)`, `Unsigned(n)`, or -`Float(n)`), no conversions are performed on either operand, and the result is -the mathematically correct result for that comparison, or `False` if either -operand is a NaN. For example: +When both operands are of standard Carbon numeric types (`Int(n)`, +`Unsigned(n)`, or `Float(n)`), no conversions are performed on either operand, +and the result is the mathematically correct result for that comparison, or +`False` if either operand is a NaN. For example: ``` // The value of `v` is True, because `a` is less than `b`, even though the @@ -252,8 +251,9 @@ comparison support for classes. In each case, the comparison is only available if it is supported by all element types. -The `Bool` type supports equality comparisons and relational comparisons. For -relational comparisons, `False` is treated as being less than `True`. +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 @@ -390,19 +390,3 @@ Disadvantages: - Introduces ambiguity when comparing Boolean values: `not cond1 == cond2` might intend to compare `not cond1` to `cond2` rather than comparing `cond1 != cond2`. - -### Disallow relational comparisons of Boolean values - -We could disallow ordered comparisons of Boolean values. - -Advantages: - -- Disallows an operation that might be unintended. -- Relational Boolean comparisons are unusual and liable to result in a - readability problem everywhere they appear. - -Disadvantages: - -- Disallows an operation that might be intended. -- Likely to make `Bool` behave differently from discriminated union types, - which are likely to be treated as data types. From 24e2c5a4f410dbfe628043f8ebe2af4b68357fc6 Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Fri, 13 Aug 2021 18:31:23 -0700 Subject: [PATCH 08/16] Based on discussion, disallow "exact" integer / floating point comparisons except where the integer type's values are a subset of the floating-point type's values. --- proposals/p0702.md | 106 +++++++++++++++++++++++++++++++++------------ 1 file changed, 79 insertions(+), 27 deletions(-) diff --git a/proposals/p0702.md b/proposals/p0702.md index f864e2ef92135..122e913a7f99b 100644 --- a/proposals/p0702.md +++ b/proposals/p0702.md @@ -22,7 +22,7 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - [Details](#details) - [Precedence](#precedence) - [Associativity](#associativity) - - [Conversions](#conversions) + - [Built-in comparisons and implicit conversions](#built-in-comparisons-and-implicit-conversions) - [Performance](#performance) - [Overloading](#overloading) - [Default implementations for basic types](#default-implementations-for-basic-types) @@ -122,6 +122,16 @@ 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: + +- A comparison produces either a mathematically correct answer or a + compilation error. +- Do not perform implicit conversions that lose information. +- Never invent an intermediate type that is larger than both operand types. + +One indirect consequence of the second and third rule is that we never convert +both operands in a comparison. + ## Details All six operators are infix binary operators. For standard Carbon types, they @@ -163,12 +173,23 @@ if (a == b == c) {} if (m > 1 == n > 1) {} ``` -### Conversions +### Built-in comparisons and implicit conversions + +When both operands are of standard Carbon integer types (`Int(n)` or +`Unsigned(n)`), the narrower operand is converted to the width of the wider +operand, then a mathematically-correct comparison is performed. -When both operands are of standard Carbon numeric types (`Int(n)`, -`Unsigned(n)`, or `Float(n)`), no conversions are performed on either operand, -and the result is the mathematically correct result for that comparison, or -`False` if either operand is a NaN. For example: +When both operands are of standard Carbon floating-point types (`Float(n)`), the +narrower operand is converted to the width of the wider operand, then a +`Float(n)` comparison is performed. + +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, it is converted to the floating-point type. Otherwise, the integer operand +is required to be a constant and is required to be exactly representable in the +floating-point type. Otherwise the comparison is rejected. + +For example: ``` // The value of `v` is True, because `a` is less than `b`, even though the @@ -176,40 +197,68 @@ and the result is the mathematically correct result for that comparison, or fn f(a: i32, b: u32) -> Bool { return a < b; } let v: Bool = f(-1, 4_000_000_000); -// The value of `w` is False, because `f` has value 999999984306749440, which -// is not exactly equal to n. +// 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; ``` -An equivalent viewpoint is that the comparison is performed in a hypothetical -sufficiently large type. For example, a comparison of `i32` against `u32` can be -performed in `i64`, and a comparison of `f32` against `i32` can be performed in -`f64`. However, no such type is required to actually exist. +More generally, 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)` or if the integer operand is a constant that is exactly + representable in the floating-point type. + +This proposal does not take an explicit stance on what it means for the integer +operand to be a constant, except that integer literals are expected to qualify +(so `f >= 0` for a floating-point value `f` is valid). For the time being, no +expression forms other than integer literals should be permitted to implicitly +convert from integer to floating-point type, although we expect this to be +revised by future proposals. -Note that this diverges from C++, which would convert both operands to a common -type first, sometimes performing a lossy conversion. +Other than the rule for converting a constant integer to floating-point, 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`. + +We support comparison operations (both equality and relational) on the following +operand types, 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)` + +When searching for a suitable comparison, we only consider performing +conversions on one of the two operands, never both. We expect for this to also +apply to [overloaded](#overloading) comparison operators for user-defined types, +but this proposal does not constrain that decision. + +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. #### Performance -The choice to not convert has a performance impact in practice, because it -exposes operations that some processors do not currently directly support. +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://godbolt.org/z/dfGe4MhEx) for implementations of several operations show the following performance on x86_64 (use the Quick-bench link in Compiler Explorer to run the benchmarks): -| Operation | Mathematical comparison time | C++ comparison time | Ratio | -| --------------- | ---------------------------- | ------------------- | ----- | -| `i64 < u64` | 2814 | 992 | 2.8x | -| `u64 < i64` | 1957 | 1012 | 1.9x | -| `f64 == i64` | 4996 | 2197 | 2.3x | -| `f64 < i64` (a) | 2012 | 2841 | 0.7x | -| `f64 < i64` (b) | 5332 | 2647 | 2.0x | - -The mathematical code sequence used for `f64 < i64` introduces a branch around a -slow path; in the benchmark, that branch should never be mispredicted. Line (a) -demonstrates the fast path and line (b) demonstrates the slow path. +| Operation | Mathematical comparison time | C++ comparison time | Ratio | +| ----------- | ---------------------------- | ------------------- | ----- | +| `i64 < u64` | 2814 | 992 | 2.8x | +| `u64 < i64` | 1957 | 1012 | 1.9x | The mixed-type operations are typically 2-3x slower than the same-type operations. However, this is a predictable performance change, and can be @@ -217,6 +266,9 @@ 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. +It is likely that better code could be generated than that currently produced in +this benchmark for these operations. + ### Overloading Separate interfaces will be provided to permit overloading equality and From b9ff0ee99e2f80df0a5418daf306dba2750947a6 Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Fri, 17 Sep 2021 13:33:22 -0700 Subject: [PATCH 09/16] Update reference for relational ordering of structs question. Co-authored-by: Geoff Romer --- proposals/p0702.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/p0702.md b/proposals/p0702.md index 122e913a7f99b..495950fe1b526 100644 --- a/proposals/p0702.md +++ b/proposals/p0702.md @@ -297,7 +297,7 @@ relational comparisons are also defined for all "data" types: Relational comparisons for these types provide a lexicographical ordering. This proposal defers to -[#561](https://github.com/carbon-language/carbon-lang/pull/561) for details on +[#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 From e2f020dfcab4534f13c65c0c987d953fc2da164b Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Fri, 17 Sep 2021 13:34:22 -0700 Subject: [PATCH 10/16] Wording flow improvement. Co-authored-by: josh11b --- proposals/p0702.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/p0702.md b/proposals/p0702.md index 495950fe1b526..8d447a3f66e40 100644 --- a/proposals/p0702.md +++ b/proposals/p0702.md @@ -186,7 +186,7 @@ narrower operand is converted to the width of the wider operand, then a 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, it is converted to the floating-point type. Otherwise, the integer operand -is required to be a constant and is required to be exactly representable in the +is required to be a constant that is exactly representable in the floating-point type. Otherwise the comparison is rejected. For example: From 219db681f0087a361d5052d045ca53c3a4c5b58a Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Fri, 17 Sep 2021 14:18:23 -0700 Subject: [PATCH 11/16] Rebase onto #820. --- proposals/p0702.md | 89 +++++++++++++++++++++++++++++++--------------- 1 file changed, 61 insertions(+), 28 deletions(-) diff --git a/proposals/p0702.md b/proposals/p0702.md index 8d447a3f66e40..b0fc2b6952332 100644 --- a/proposals/p0702.md +++ b/proposals/p0702.md @@ -23,6 +23,8 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - [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) @@ -124,13 +126,18 @@ 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. -- Do not perform implicit conversions that lose information. -- Never invent an intermediate type that is larger than both operand types. -One indirect consequence of the second and third rule is that we never convert -both operands in a comparison. +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 @@ -185,9 +192,11 @@ narrower operand is converted to the width of the wider operand, then a 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, it is converted to the floating-point type. Otherwise, the integer operand -is required to be a constant that is exactly representable in the -floating-point type. Otherwise the comparison is rejected. +type, it is converted to the floating-point type. Otherwise the comparison is +rejected. + +Comparisons involving integer and floating-point constants are not covered by +these rules and are discussed below. For example: @@ -204,28 +213,26 @@ let n: i64 = 1_000_000_000_000_000_000; let w: Bool = f == n; ``` -More generally, we support the following implicit conversions: +#### 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)` or if the integer operand is a constant that is exactly - representable in the floating-point type. + `Int(n)`. -This proposal does not take an explicit stance on what it means for the integer -operand to be a constant, except that integer literals are expected to qualify -(so `f >= 0` for a floating-point value `f` is valid). For the time being, no -expression forms other than integer literals should be permitted to implicitly -convert from integer to floating-point type, although we expect this to be -revised by future proposals. +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`. -Other than the rule for converting a constant integer to floating-point, 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 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. -We support comparison operations (both equality and relational) on the following -operand types, for each suitable value `n`: +We support comparison operations -- both equality and relational -- on the +following operand types, for each suitable value `n`: - `Int(n)` vs `Int(n)` - `Unsigned(n)` vs `Unsigned(n)` @@ -233,10 +240,10 @@ operand types, for each suitable value `n`: - `Unsigned(n)` vs `Int(n)` - `Float(n)` vs `Float(n)` -When searching for a suitable comparison, we only consider performing -conversions on one of the two operands, never both. We expect for this to also -apply to [overloaded](#overloading) comparison operators for user-defined types, -but this proposal does not constrain that decision. +When searching for a suitable comparison between built-in types, we consider +performing implicit conversions on each of the two operands, but never both. We +expect for this to also apply to [overloaded](#overloading) comparison operators +for user-defined types, but this proposal does not constrain that decision. The two kinds of mixed-type comparison may be [less efficient](#performance) than the other kinds due to the slightly wider domain. @@ -246,6 +253,24 @@ 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, 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 @@ -282,9 +307,11 @@ proposal. As non-binding design guidance for such a proposal: 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. + values. We should decide whether we wish to support such uses. ### Default implementations for basic types @@ -314,14 +341,20 @@ in general. That decision is left to a future proposal. - 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 convert operands of a comparison operator removes a - source of bugs caused by unintended lossy conversions. + - 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:_ From 42cc532c8d5fa71d1c185ee2fd4fc5226ab4d93e Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Fri, 17 Sep 2021 14:45:23 -0700 Subject: [PATCH 12/16] Improve wording and precision. --- proposals/p0702.md | 61 +++++++++++++++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/proposals/p0702.md b/proposals/p0702.md index b0fc2b6952332..02371cbc82a62 100644 --- a/proposals/p0702.md +++ b/proposals/p0702.md @@ -182,21 +182,18 @@ if (m > 1 == n > 1) {} ### Built-in comparisons and implicit conversions -When both operands are of standard Carbon integer types (`Int(n)` or -`Unsigned(n)`), the narrower operand is converted to the width of the wider -operand, then a mathematically-correct comparison is performed. +Built-in comparisons are permitted: -When both operands are of standard Carbon floating-point types (`Float(n)`), the -narrower operand is converted to the width of the wider operand, then a -`Float(n)` comparison is performed. +- 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 -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, it is converted to the floating-point type. Otherwise the comparison is -rejected. - -Comparisons involving integer and floating-point constants are not covered by -these rules and are discussed below. +In each case, the result is the mathematically-cocrrect answer. This applies +even when comparing `Int(n)` with `Unsigned(m)`. For example: @@ -213,6 +210,9 @@ 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), @@ -231,8 +231,10 @@ Additionally #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. -We support comparison operations -- both equality and relational -- on the -following operand types, for each suitable value `n`: +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)` @@ -240,10 +242,31 @@ following operand types, for each suitable value `n`: - `Unsigned(n)` vs `Int(n)` - `Float(n)` vs `Float(n)` -When searching for a suitable comparison between built-in types, we consider -performing implicit conversions on each of the two operands, but never both. We -expect for this to also apply to [overloaded](#overloading) comparison operators -for user-defined types, but this proposal does not constrain that decision. +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. From 34c2f7bbf6b92bedca2d7f87dd0f56535008367a Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Fri, 17 Sep 2021 14:46:32 -0700 Subject: [PATCH 13/16] Explain why it's useful to allow overriding != separately from ==. --- proposals/p0702.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/proposals/p0702.md b/proposals/p0702.md index 02371cbc82a62..a466f17701670 100644 --- a/proposals/p0702.md +++ b/proposals/p0702.md @@ -326,6 +326,8 @@ 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, From 3813ea745124e93b66b7338ff2cd56952d9ffbd5 Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Fri, 17 Sep 2021 18:15:03 -0700 Subject: [PATCH 14/16] Linkify references to #820. Co-authored-by: josh11b --- proposals/p0702.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/proposals/p0702.md b/proposals/p0702.md index a466f17701670..902a149c8d5d4 100644 --- a/proposals/p0702.md +++ b/proposals/p0702.md @@ -227,7 +227,8 @@ we support the following implicit conversions: 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 permits conversions from certain kinds of integer and +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. @@ -278,7 +279,8 @@ wider type than either of the operand types. #### Comparisons with constants -As described in #820, integer constants can be implicitly converted to any +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 From ffe8e55868a5d58a0231c489ed90618ff75f927f Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Fri, 17 Sep 2021 18:15:39 -0700 Subject: [PATCH 15/16] Fix typo. Co-authored-by: josh11b --- proposals/p0702.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/p0702.md b/proposals/p0702.md index 902a149c8d5d4..d78c0c159b62f 100644 --- a/proposals/p0702.md +++ b/proposals/p0702.md @@ -192,7 +192,7 @@ Built-in comparisons are permitted: if all values of the integer type can be exactly represented in the floating-point type -In each case, the result is the mathematically-cocrrect answer. This applies +In each case, the result is the mathematically-correct answer. This applies even when comparing `Int(n)` with `Unsigned(m)`. For example: From 640a7461d953544d82f7a62bade357c1d6061a82 Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Wed, 22 Sep 2021 12:39:17 -0700 Subject: [PATCH 16/16] Update benchmark to use a faster mixed-type comparison. Reflow text modified by github suggestions. --- proposals/p0702.md | 52 ++++++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/proposals/p0702.md b/proposals/p0702.md index d78c0c159b62f..e6ca7b1e873f4 100644 --- a/proposals/p0702.md +++ b/proposals/p0702.md @@ -192,8 +192,8 @@ Built-in comparisons are permitted: 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)`. +In each case, the result is the mathematically-correct answer. This applies even +when comparing `Int(n)` with `Unsigned(m)`. For example: @@ -228,9 +228,8 @@ 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. +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 @@ -280,11 +279,10 @@ 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: +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. @@ -301,23 +299,33 @@ practice, for example in templated code where the literal is sometimes in range. 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://godbolt.org/z/dfGe4MhEx) for implementations of -several operations show the following performance on x86_64 (use the Quick-bench -link in Compiler Explorer to run the benchmarks): +[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` | 2814 | 992 | 2.8x | -| `u64 < i64` | 1957 | 1012 | 1.9x | +| `i64 < u64` | 1636 | 798 | 2.0x | +| `u64 < i64` | 1956 | 798 | 2.5x | -The mixed-type operations are typically 2-3x slower than 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 execution times here are computed as operation time minus no-op time. -It is likely that better code could be generated than that currently produced in -this benchmark for these operations. +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