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 | < <= > >= |
left |
| 5 | \| |
left |
| 6 | ^^ |
left |
| 7 | & |
left |
| 8 | << >> |
left |
| 9 | + - |
left |
| 10 | * / % |
left |
| 11 | ^ |
right |
2 ^ 3 ^ 2 is 2 ^ (3 ^ 2).
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.
Four forms bind tighter than every binary operator: a function call f(x), an
index or slice a[i], a field access p.x, and a method call p.len(). 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, and
fs[0](x) calls the element it indexed.
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
as_int() or as_float().
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
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.
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, 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 all three
assignable targets are 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. Both bounds must be int. Either bound may equal the length, and a slice
that falls outside the value raises slice_out_of_bounds. There is no a[i..]
form; give the upper bound.
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.
Maps are not indexed with []. Use the get method; see
collections.
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, or a
function inside either.
A host or dylib block's name takes either one, so app.rows(id) and
app::rows(id) are the same call, and a variable of that name takes the dot
back for its own methods. 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. Where an expression is read it is bitwise or. Nothing is ambiguous about that, because a type and an expression never stand in the same place....marks a variadic host function in ahostblock. See embedding.->gives the return type in adyliborhostsignature, and=>separates amatchpattern from its body.
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.