Unsigned arithmetic wrapuN- lower precedenceCarbon needs a set of arithmetic operators in order to perform basic calculations on various kinds of built-in and user-defined numbers and number-like values: integers, real numbers, complex numbers, vectors, matrices, and so on.
Conventions for arithmetic operators have a very long tradition. The following symbols have common meaning across C, C++, Java, JavaScript, Rust, Swift, and many other languages:
+ and - mean addition and subtraction, as in mathematics. Unary -
forms a negated value -- an additive inverse. Sometimes, unary + is
permitted, as a no-op.* means multiplication, diverging from the use of '×', '⋅', or simply
juxtaposition in normal mathematical notation. However, this symbol does
have some visual similarity to '×'./ means division, diverging from the use of '÷', fraction notation, or an
exponent of -1 in mathematics. However, this symbol does somewhat visually
resemble fractional notation that has "fallen over" to the left.% means remainder, diverging from the use of the word "mod" in
mathematics, and, perhaps confusingly, resembling the '÷' division operator
from mathematics.Efficient integer types often have finite bounds on the numbers they can represent, and we expect Carbon's to be no different. This presents a problem for operations that might produce values outside those bounds: what should the behavior of a primitive arithmetic operation be if the result cannot be represented? Moreover, some operations, such as division by zero, have no mathematically-defined result.
Different languages take different approaches to this problem.
Wrapping<T> type, where T is a signed or unsigned integer
type, that provides arithmetic with guaranteed two's complement wrapping
semantics.&, such as big_num &* other_big_num, to request two's
complement wrapping behavior instead.100000001 * 100000001 evaluates to 10000000200000000 not
to 10000000200000001.Carbon will provide the five usual binary arithmetic operators -- +, -, *,
/, and % -- with their usual semantics. A unary minus - operator will also
be provided, but no unary plus + operator.
| Notation | Meaning |
|---|---|
-a |
Negation |
a + b |
Addition |
a - b |
Subtraction |
a * b |
Multiplication |
a / b |
Division |
a % b |
Modulo / remainder |
These operators follow the same precedence rules as in C-family languages --
multiplicative operators bind tighter than additive operators, and negation
binds tighter than multiplicative operators. Unlike in other C-family languages,
no precedence is defined between % and other binary operators, so
a + b % c * d is an error, and does not mean a + (b % c) * d as it would in
C++.
Unlike in C++, lossy conversions are never performed. Instead, built-in
arithmetic is not permitted unless one of the operand types can represent all
the values of the other operand. The calculation is performed in that operand
type; there is no implicit widening to Carbon's equivalent of int.
Unsigned integer types uN model arithmetic modulo 2N, and we
strongly advise that they are not used except when those semantics are desired,
such as in random number generation, cryptography, hashing, and similar domains.
For signed integer types iN, it is a programming error if overflow occurs. We
guarantee that in development build modes such errors will result in a runtime
trap, and that in hardened build modes the result will either be a trap or the
two's complement value.
Initially, no support will be provided for wrapping signed integer arithmetic, nor for non-wrapping unsigned integer arithmetic. This can be added by a future proposal if we find there is sufficient demand for either.
Floating-point types use IEEE 754 semantics, with round-to-nearest rounding mode, no signaling NaNs, and no floating-point exceptions.
This proposal takes no position on whether the + operator is supported on
strings to perform concatenation.
See the changes to the design for more details; the rest of this proposal will focus on the rationale and alternatives.
We want Carbon to provide a close analogue of standard mathematical expression
notation, with some unsurprising set of operators. While accessibility to
beginners is important, the most important audience for which the operators set
should be unsurprising is programmers with C++ or Rust experience. The choice of
+, -, *, /, and % is ubiquitous among programming languages, and any
significant change to this symbol set for the benefit of programming novices
with a mathematical background would be a detriment to those with a background
in other programming languages.
Therefore we choose the conventional symbol set.
Carbon prioritizes fast and predictable performance above all other factors. Allowing an optimizer to assume that certain forms of arithmetic do not result in overflow can improve program performance and allow the generation of smaller code. Moreover, while in general we only want to provide the means for accessing the fastest possible implementation of an operation, we expect integer arithmetic to be so ubiquitous that it's important that the most efficient option be the default option.
At the same time, it's important that Carbon code can be used in safety-critical scenarios where unbounded undefined behavior on integer arithmetic is problematic, so in a hardened build mode, we provide stronger guarantees.
We could define that integer operators always produce a sufficiently wide result type such that overflow never occurs, and defer all overflow handling -- checking for out-of-bounds values, wrapping around, or a programmer assertion that overflow simply doesn't happen -- until the end of the computation or some explicit step.
For example:
fn F(a: i32, b: i32, c: i32) {
// a * b is computed in i63,
// ... + c is computed in i64.
// =! is an assertion that the value is in-range.
let d: i32 =! a * b + c;
// Same, but =% says to wrap around.
let e: i32 =% a * b + c;
// If the value doesn't fit in the specified type, pattern-matching fails.
let f: i32 = a * b + c else { return; }
}
We could put a limit on how large an intermediate type can be, and reject if it would require a larger intermediate operand than the largest we can efficiently support.
This approach seems quite promising, but close inspection finds a number of non-trivial issues.
Advantages:
=% or =!, all intermediate arithmetic other
than division and remainder can be done in the result type, avoiding the
need for wide computations.Disadvantages:
=! to mean
"assign assuming the RHS fits into the LHS", there doesn't seem to be any
good generalization of =% to other situations and types.i64 / u64. While 128-bit arithmetic is typically available on our target
architectures, use of it will often be less efficient and may increase
register pressure. Calculations as simple as (a + b) / c may be rejected
if the operands are already in the largest efficient type.-i32.Min and i32.Min / -1 don't fit in i32. The following
approaches to this problem were considered:
Min value from iN types, so the negative and positive
range are identical. However, this would severely violate programmer
expectations, for example in some important bit-manipulation cases.i32 produces a type that can
represent [-231+1, 231], which still fits in 32 bits.
However, this would add significant complexity to the type system, and with
this approach, division would still increase the bit width: for example,
a / b, where a and b are iNs, has 2N+1 distinct possible
values. This is especially surprising because integer division is usually
expected to make a number smaller!! and % annotations become quite viral: they would be needed not
only in initialization and assignment, but likely also in case, return,
and other initialization contexts not using =. As an alternative, the
annotation could be put on the outermost arithmetic operator, but that is
likely to be syntactically awkward, especially in unparenthesized
expressions such as a + b +% c.%
or always use !.
! means that the developers see no benefit compared to
the behavior as proposed here, and nonetheless pay an ergonomic cost.% means that we lose any ability to distinguish between
intentional wraparound and overflow, making a class of bugs that would
otherwise be easily detectible be undetectable without making the
program behavior correct.We could say that overflow errors always result in program termination, possibly with the permission for the compiler to give the mathematically correct result instead if it so chooses.
This would result in slower and larger code being generated in a lot of cases. Some optimizations would still be possible if the optimizer could ensure that it only increased the set of cases for which the correct result is given, but current optimizers are not well-suited to perform that task.
Given Carbon's emphasis on performance, this approach is rejected without prejudice until we have a demonstration that no important performance metric is harmed by it.
Instead of making overflow a programming error, we could define it as two's complement. This is, for example, the approach taken by Java.
The major problem with this approach is that it makes erroneous wrapping and intentional wrapping indistinguishable. This would make finding such bugs much harder for readers of the code, and all but impossible for static and dynamic analysis tools. Making overflow issues programming errors allows problems to be caught earlier and more reliably.
We could follow Rust and say that overflow is an error, but that we promise we'll either catch it or give two's complement semantics. This approach is currently rejected for the same reason we reject guaranteeing we catch all cases where we can't give a correct result.
Unsigned arithmetic wrapWe could treat Unsigned(N) like Integer(N), and make it an error by default
if it overflows. However, this doesn't seem like a great fit for the problem
domain.
There are, broadly speaking, two different classes of use cases we want to support:
iN to be used for all such cases, and do not want to
treat the non-negative cases as a distinct and special type.The second class is certainly rarer than the first, but contains many important use cases.
The typical arguments for using an unsigned type for the first class of use cases are:
Additionally, providing unsigned types for which overflow is an error introduces
a much larger risk of that error state being inadvertently reached than for
signed types, because calculations typically involve numbers that are close to
zero, which is far from overflowing in signed types but near to overflowing in
unsigned types. For example, a calculation such as a - b + c may be known to
always produce a non-negative result, and the developer may be tempted to use
non-negative non-wrapping types for a, b, and c, but doing so introduces
the risk that the intermediate a - b calculation is negative. By contrast,
overflow would only occur in a signed calculation if the numbers involved were
very large.
We could provide distinct types for wrapping versus non-wrapping use cases, independent of the choice of signedness. This approach is being followed by Rust.
Advantages:
Disadvantages:
impls
need to be defined for each operation, twice as many overloads in each
overload set, and so on.uNFor arithmetic purposes, we treat uN as the integers modulo 2N.
There is no single canonical total order for that ring, and because
multiplication by even numbers loses information by discarding the high-order
bit, not all non-zero numbers have
multiplicative inverses,
and so division is not well-defined. We could be more mathematically pure by
refusing to implement < and friends for uN, and similarly refusing to
support /.
However, the uN types exist as much for pragmatic purposes as for mathematical
ones. Making the choice to treat all uN values as non-negative is somewhat
arbitrary, but is both unsurprising to those coming from C++ and useful for the
cases where some ordering is desired.
- lower precedenceIn this proposal, - a * b is interpreted as (-a) * b. We could instead
interpret it as - (a * b).
Advantages:
Disadvantages:
a * -b is invalid, because * has higher precedence than -.Comparison to other languages:
- and unary +)
higher precedence than any binary operator.- higher precedence than multiplication.- more tightly than multiplication but less tightly
than exponentiation, so - a * b is (- a) * b but - a ** b is
- (a ** b).- the same precedence as binary - and rejects
a * - b.C and C++ include a unary + operator. In principle, this operator permits
lists of numbers to be written with an operator attached to each:
int arr[] = {
-100,
-50,
+20,
+400
};
... but in practice this use case is rare at best, and unary + in C++ is
instead mainly used to coerce operands to prvalues of built-in types. For
example, auto *p = +[]{ /*...*/ }; might be used to create a function pointer
(auto deduction would fail without the unary + operator), and
min(+Class::static_member, x) might be used to force Class::static_member to
be loaded, to avoid requiring the static member to be defined.
Such usage of + is a design wart that we need not replicate. If we want a
mechanism to decay an operand, we can pick a better name for it than +.
It would be possible and sometimes useful to support the % operator for
floating-point types.
Advantages:
f1 % f2 would be a more concise notation than a call to an
fmod function (however it is named).Disadvantages:
We could follow Python3 and provide an / operator for integers that produces a
floating-point (or perhaps rational) type. This would be mathematically clean:
ignoring overflow and precision loss, all standard properties for division would
be maintained. Or we could follow Haskell and refuse to provide an / operator
between integers on the basis that such division is not mathematically defined
for that type. Or we could provide a division operator that produces a pair of
dividend and modulo, or an Optional(Int) that is absent whenever the division
is inexact. In each case, functionality not available through operators could be
provided with named functions instead.
All of these options are likely to be surprising to programmers coming from C++, to a level that outweighs the perceived benefit.
There are multiple somewhat-reasonable ways to define division operations for
signed integers (whether we provide those operations as operators or library
functions). Assuming operator notation for now, and that we define modulo as
a % b == a - a / b * b, the following options all have merit:
| Property | Round towards zero (truncating division) | Round towards negative infinity (floor division) | Round based on sign of divisor[1] (Euclidean division) |
|---|---|---|---|
(-a) / b ==a / (-b) |
:+1: Yes | :+1: Yes | No |
(-a) / b ==-(a / b) |
:+1: Yes | No | :+1: Yes |
a / (-b) ==-(a / b) |
:+1: Yes | No | No |
(a + k * b) / b== a / b + k |
No | :+1: Yes | :+1: Yes |
Sign of a % b |
Same as a |
:+1: Same as b |
:+1: Never negative |
| x86 instruction? | :+1: Yes: cqo (or similar) + idiv |
First option + fixup:s * (a / (s * b)) where s is sign(a) * sign(b)[2] |
First option + fixup:a / b - (a % b < 0) |
| LLVM IR + optimization support | :+1: Yes | No | No |
| Use in existing languages | C, C++, Rust, Swift quotRem in Haskell |
// and % in Python / in Python 2 only divMod in Haskell |
None? |
The cells marked :+1: suggest generally desirable properties. For further reading, see this Microsoft Research paper.
Our options here are as follows:
/, and (optionally)
pick one of the above interpretations as the meaning of %); perhaps
provide the others as library functions or as additional operators.Note that we are not required to provide a % that is consistent with our
chosen / operator. (We could pick truncate-towards-zero for / and
truncate-towards-negative-infinity for %, for example.) There is
long-established tradition here, but it's unclear to what extent practicing
programmers really care about the relationship between / and %.
It is likely that most Carbon code that performs division and modulo between
signed integer types does not actually care about what happens when either
operand is negative. Therefore, following our goals of supporting
high-performance code and current CPU architectures, we will choose to implement
/ and % as division with truncation towards zero. The other variants can be
provided by a library function, if at all.
[1]: That is: for positive divisors, round towards negative infinity; for negative divisors, round towards positive infinity.
[2]: Here, sign(a) is 1 if a >= 0 and is -1 if a < 0.
Under this proposal, division and multiplication are in the same precedence
group, and are left-associative: a * b / c * d is ((a * b) / c) / d, and not
(a * b) / (c * d) or some other grouping.
It's not feasible to provide a different interpretation here due to the risk of confusing developers migrating from other languages. However, we could reject such expressions and require explicit parentheses.
While the value of accepting code such as this is relatively low, the fact that both existing programming languages and common mathematical education treat multiplication and division as the same, left-associative, precedence group means that it's unlikely to be a significant burden to expect Carbon developers to remember this precedence rule.
In most languages with the set of arithmetic operators discussed in this
proposal, % is considered a multiplicative operator, and so a sequence of *,
/, and % operators is processed left-to-right. In some sense this is
reasonable: % is notionally performing a division, after all. Moreover, in a
code search, I was unable to find evidence that it's common for precedence
errors with % to be checked in to source control, and C++ compilers don't have
warnings for mixing % with + without parentheses.
Giving % and / the same precedence also allows some kinds of code to be
written symmetrically:
char two_digit_number[] = {
'0' + m / 10,
'0' + m % 10,
0
};
char four_digit_number[] = {
'0' + n / 1000,
'0' + n / 100 % 10,
'0' + n / 10 % 10,
'0' + n % 10,
0
};
With minimal parentheses, that example would be written as follows under this proposal:
var two_digit_number: Array(Char, 3) = (
'0' + m / 10,
'0' + (m % 10),
0
);
var four_digit_number: Array(Char, 5) = (
'0' + n / 1000,
'0' + ((n / 100) % 10),
'0' + ((n / 10) % 10),
'0' + (n % 10),
0
);
We could use the same precedence rule as other languages, and permit examples similar to the above to be written without any parentheses.
However, our rule for precedence is:
For every combination of operators, either it should be reasonable to expect most or all developers who regularly use Carbon to reliably remember the precedence, or there should not be a precedence rule.
It is not clear that a precedence rule that gives a defined meaning to, for
example, a + b % c + d would satisfy this rule. Moreover, in mathematics, the
"mod" operator generally binds very loosely: in 'n = m + 1 (mod 5)', the '(mod
5)' applies to the entire equality, certainly not to the '1'.
Therefore, we do not give modulo higher precedence than addition, and require parentheses when mixing the two. This decision should be revisited if it is a significant source of friction in practice.
We could use a different spelling for the modulo operation. For example, we
could spell it as mod.
Advantages:
% symbol for other uses that may be more prevalent than
modulo. However, we would need to be cautious when adding any alternative
uses to avoid confusion for people who expect % to mean modulo.Disadvantages:
mod would break our loose convention of using keywords for
non-overloadable operators and operator symbols for overloadable operators.
Using a function would substantially increase the verbosity of certain kinds
of code.We could provide distinct operators with wrapping semantics for types where
overflow is normally a programming error. For example, Swift provides
overflow operators
&+, &-, and &* for this purpose.
This proposal takes no position on whether this would be useful. However, given that in this proposal, unsigned types already have wrapping semantics, the pressure to provide operators for signed types with those semantics is somewhat reduced.
It would be useful to provide a way to perform an arithmetic operation if possible, and to provide a separate codepath to handle the case where the arithmetic would have overflowed. For example, we could imagine the Carbon standard library providing a facility such as:
fn AddWithOverflow[N:! BigInt](a: Integer(N), b: Integer(N)) -> Optional(Integer(N));
While this would undoubtedly be useful, this proposal provides no facility for this operation.