Operators¶
Every operator candela has, with its precedence, the operand types it accepts, and the value it produces.
Precedence¶
Binary operators bind in this order, loosest first. All are left-associative
except ^.
| Level | Operators | Associativity |
|---|---|---|
| 1 | \|\| |
left |
| 2 | && |
left |
| 3 | == != |
left |
| 4 | < <= > >= is as |
left |
| 5 | \| |
left |
| 6 | ^^ |
left |
| 7 | & |
left |
| 8 | << >> |
left |
| 9 | + - |
left |
| 10 | * / % |
left |
| 11 | ^ |
right |
2 ^ 3 ^ 2 is 2 ^ (3 ^ 2).
is and as take a type on their right rather than an expression, at the
level of the comparisons: a + b as int is (a + b) as int, v as int + 1 is
(v as int) + 1, and v is int && v > 0 tests before it compares. See type
tests and downcasts.
The order is Rust's. The bitwise operators bind tighter than a comparison, so
flags & mask == mask asks whether flags & mask equals mask, and the shifts
bind looser than +, so 1 << n + 1 shifts by n + 1.
Five forms bind tighter than every binary operator: a function call f(x), an
index or slice a[i], a field access p.x, a method call p.len(), and x?.
They attach to the term they follow, so a.b[0] ^ 2 raises a.b[0] to the
power of two, and they chain: adder(1)(2) calls what adder(1) returned,
fs[0](x) calls the element it indexed, and m.get(k)?.len() reads the length
of the value a lookup found.
The prefix operators -, ! and ~ bind tighter than ^ and looser than the
postfix forms. -a ^ 2 is (-a) ^ 2 and !ok == done is (!ok) == done. Use
parentheses when you want the other reading.
Types are never mixed¶
Both operands of an arithmetic, bitwise or comparison operator must already have
the same type. candela does not promote int to float, so 1 + 2.0 is a
compile error. Convert first with float() or int():
int is a signed 64-bit integer and float is a 64-bit binary floating-point
number. Arithmetic on int wraps at the ends of that range. See
types.
That rule is about the built-in types. A struct or an enum of your own settles what an operator on it accepts, including a second operand of another type; see user types below.
Arithmetic¶
| Operator | Operand types | Result |
|---|---|---|
+ |
int, int |
int |
+ |
float, float |
float |
+ |
string, string |
string, the two joined |
+ |
T[], T[] |
T[], a new array holding both |
- * / % ^ |
int, int |
int |
- * / % ^ |
float, float |
float |
+ on arrays builds a new array and leaves both operands untouched:
Integer division truncates towards zero, and % takes the remainder with the
sign of the left operand. Integer division or remainder by zero raises the
runtime errors division_by_zero and modulo_by_zero, both catchable; see
error handling. Float division and remainder
follow IEEE 754 and produce infinities or NaN instead of raising.
^ raises the left operand to the power of the right. With int operands the
exponent must not be negative: a negative literal is rejected before the program
runs, and a negative value only known at run time raises the catchable
negative_exponent. Use floats when you want a fractional result.
Integer arithmetic wraps on overflow.
An any value cannot be used as an arithmetic operand. Narrow it first with
v as int or v as float, or test it with is.
Bitwise¶
&, |, ^^, ~, << and >> work on the bits of an int and produce an
int. ^ is the power operator, so xor takes the doubled spelling ^^, the
way && and || take theirs.
| Operator | Operand types | Result |
|---|---|---|
& |
int, int |
int, the bits set in both |
\| |
int, int |
int, the bits set in either |
^^ |
int, int |
int, the bits set in one of the two |
~ |
int |
int, every bit flipped |
<< |
int, int |
int, the bits moved up |
>> |
int, int |
int, the bits moved down |
No other operand type is accepted, and the types are never mixed: 1.5 & 1 is
the same compile error 1.5 * 1 is, and an any value has to be narrowed with
v as int first.
let read = 4;
let write = 2;
let mode = read | write;
print(mode & read == read); // true
print(~0); // -1
int is signed, so >> is arithmetic: the vacated places take the sign bit,
and -16 >> 2 is -4. << fills with zeroes and wraps like the rest of
integer arithmetic, so 1 << 63 is the smallest int.
An int holds 64 bits, so both shifts take a count between 0 and 63. A count
written as a literal outside that range is refused before the program runs; a
count only known at run time raises the catchable shift_count_out_of_range.
Comparison¶
<, <=, > and >= produce a bool. Both operands have to be the same
type, and that type has to be one of three:
| Operator | Operand types | Result |
|---|---|---|
< <= > >= |
int, int |
bool |
< <= > >= |
float, float |
bool |
< <= > >= |
string, string |
bool |
Strings order by their bytes, which is the order sort puts a list of strings
in. A string that prefixes another comes first, so "ab" < "abc". Bytes are
not letters: every uppercase ASCII letter comes before every lowercase one, so
"Z" < "a", and every ASCII character comes before a character that takes more
than one byte. Lowercase both sides when you want a case-insensitive order.
fn main() {
let names = ["pear", "Apple", "fig"];
names.sort();
print(names); // ["Apple","fig","pear"]
print("fig" < "pear"); // true
}
There is no ordering on arrays, maps, structs or enums.
Equality¶
== and != produce a bool and accept operands of any type. Two values of
different types are never equal, so "5" == 5 is false and "5" != 5 is true.
That holds whether the mismatch is visible at compile time or only shows up at
run time through a parameter whose type comes from the call. Comparing values of
the same type is the case worth writing.
| Operand type | How it compares |
|---|---|
int, float, bool, null |
by value |
string |
by contents |
T[] |
by length, then element by element |
| struct | by type, then field by field |
| enum | by variant, then payload |
| map | by length, then key by key |
Comparison recurses, so nested collections compare all the way down.
Floats compare by their exact representation: 0.0 == -0.0 is false, and any
comparison involving NaN is false.
Logical¶
&&, || and ! take bool operands and produce a bool. No other type is
accepted, so there is no truthiness; write the comparison out:
&& and || short-circuit wherever they appear: the right operand is not
evaluated when the left one already settles the answer. That holds in a
condition, in a let, in an argument, and in a returned expression alike, so a
right operand with a side effect runs only when it is reached.
Type tests and downcasts¶
v is T produces a bool: whether v holds a T. v is Some(x) tests an
enum value's variant with the pattern a match arm writes. v as T produces
v typed T, and raises the catchable bad_downcast when it holds another
type. T is any type an annotation can name, read up to the end of the type, so
v is int | string tests for the union.
Where the test held, a tested variable has the type T and a variant's names
hold its payload: in the body of the if or while it is the condition of,
and to the right of an && after it. See
types.
User types¶
A struct or an enum defines an operator by declaring a method named with the symbol, and the operator on a value of that type calls it. See methods.
impl Vec2 {
fn +(self, other: Vec2) -> Vec2 {
return Vec2 { x: self.x + other.x, y: self.y + other.y };
}
}
| Operators | Form |
|---|---|
+ - * / % ^ & \| ^^ << >> |
fn op(self, other) |
== < <= |
fn op(self, other) -> bool |
- ~ |
fn op(self) |
- is the one symbol with both forms, and the parameter count says which is
being declared. !=, > and >= have no method: they come from ==, < and
<=. A derived comparison swaps its operands, so it evaluates the right one
first.
The other operand is of the receiver's type unless the method declares
otherwise. Only the left operand picks the method, so 2.0 * v stays a type
error however Vec2 defines *. The built-in types are closed: an operator
method inside impl int or impl string is a compile error. &&, ||, !,
= and the compound assignments are not defined by a type; the compound forms
apply the matching operator instead.
Assignment¶
= assigns to a variable, an array element, a map entry, or a struct field:
Assignment is a statement, not an expression, so it produces no value and cannot be chained.
The compound forms +=, -=, *=, /=, %=, ^=, &=, |=, ^^=, <<=
and >>= apply the matching binary operator to the current value and assign the
result. x += 1 is x = x + 1, with the same type rules, and every
assignable target is allowed:
A compound form on a value of your own type applies the operator that type
defines, so v += w reaches its + method and needs nothing of its own.
Use let to introduce a name and = to change one; see
variables.
Indexing and slicing¶
a[i] reads one element of an array, or one character of a string as a
one-character string. The index must be an int and must be within bounds; a
negative index does not count from the end, and an out-of-range index raises the
catchable index_out_of_bounds.
a[i..j] takes a slice from i up to but not including j. a[..j] starts at
zero and a[i..] runs to the end. The bounds must be int. Either bound may
equal the length, and a slice that falls outside the value raises
slice_out_of_bounds.
The same .. closes a struct literal, Point { x: 1, ..origin }, where it takes
the fields the literal does not write from another value; see
types.
Positions on a string count characters, so word[0..4] on a four-letter word
ending in an accented vowel takes the whole word, and word[3] reads the
accented vowel. A character is a Unicode scalar value, whatever it takes to
store.
m[k] reads the value a map holds under the key k, which has to be of the
map's key type, and raises unknown_map_key when the map holds no such key.
m[k] = v writes the entry, adding it or replacing its value. m.get(k) is the
lookup that answers None instead of raising; see
collections.
Propagation¶
x? takes an Option or a Result. On a Some or an Ok it is the value
inside; on a None or an Err it returns that value from the enclosing
function at once. The function has to return the same enum, with the same
error type for a Result, so ? works only inside one:
fn first_word_length(words: string[]) -> Option<int> {
let w = words.first()?;
return Some(w.len());
}
See error handling.
Access and calls¶
. reads a struct field or calls a method on a value. :: separates the parts
of a namespaced name: a module bound with import ... as, an enum variant, a
host or dylib block, a host block's struct, or a function inside any of
them.
A host or dylib block's name is a namespace, so its functions are called
as app::rows(id); app.rows(id) is a compile error. A variable of that name is
a value, and a dot on it calls the value's own method. See
C libraries.
See methods, enums and modules.
Ranges¶
.. builds the range of a for loop and appears nowhere else. Both bounds must
be int, and the end is exclusive. for i in ..n starts at zero.
See control flow.
Symbols that are not operators¶
|is not an operator where a type is read: it separates the members of a union type, as inint | string, including the type afterisandas. In amatchpattern it separates alternatives, as in1 | 2 => .... Everywhere else an expression is read it is bitwise or; parenthesise to use bitwise or inside a pattern,(a | b).asafter an import's path names the module's alias, as inimport "std/json";....marks a variadic host function in ahostblock. See embedding.->gives the return type in adyliborhostsignature.=>means "evaluates to": it separates amatchpattern from its body, and gives a function a body of one expression, as infn(x) => x * 2.
Constant expressions¶
An expression built only from literals is folded while the file is parsed, so it
costs nothing at run time. Folding applies the same type rule as everything
else: 2.0 ^ 3 is rejected for mixing a float with an int, exactly as it
would be if the operands were variables.
Folding also turns four mistakes into compile errors rather than runtime ones:
dividing by a literal 0, taking the remainder by a literal 0, raising an
integer to a negative literal exponent, and shifting by a literal count outside
0 to 63. Those apply to integer arithmetic. A
float divided or remaindered by 0.0 follows IEEE 754 and produces an
infinity or NaN, so it is folded rather than rejected.