Operators
Precedence
Tightest first. An operator binds its operands more tightly than anything below it in this table.
| Level | Operators | Groups |
|---|---|---|
| 15 | a[i] |
left |
| 14 | f(x) |
left |
| 13 | a.b a::b |
left |
| 12 | !a -a +a |
right |
| 11 | * / % |
left |
| 10 | + - |
left |
| 9 | is |
left |
| 8 | as |
left |
| 7 | < <= > >= |
left |
| 6 | == != |
left |
| 5 | && |
left |
| 4 | \|\| |
left |
| 2 | a ? b : c |
right |
| 1 | = += -= *= /= %= |
right |
Parentheses group, and a grouped expression is whatever is inside it.
Important
as and is bind looser than arithmetic, which is not how they read. Both
take a type on the right rather than an expression, so everything the arithmetic
operators have already claimed is the left operand.
a + b as i64 is (a + b) as i64, not a + (b as i64).
use std;
let a: i32 = 7;
let b: i32 = 2;
let together = a + b as i64; // (a + b) as i64
let separate = a + (b as i64); // the other reading, written out
println($"{together} {separate}");
The two answer the same here, but they do not in general: the cast applies to a
different value, so a sum that overflows an i32 overflows before it widens.
Write the parentheses when the difference matters.
Member access binds tighter than unary, so -point.x negates the field rather
than the value. A call binds tighter than the access it is part of, which is why
value.doubled() reads as one thing.
Arithmetic
| Operator | On | Produces |
|---|---|---|
* / % - |
two numbers | their coercion |
+ |
two numbers | their coercion |
+ |
two strings | string |
+ |
a string and a char |
string |
use std;
println($"{7 * 6}");
println($"{7 / 2}");
println($"{7 % 2}");
println("Hello, " + "world");
println("x" + 'y');
+ joins text only for those two cases. To put a number or a bool into a
string, interpolate it.
let text = "count: " + 3;
Comparison
<, <=, > and >= compare two numbers and answer a bool. A char takes
part as its code point.
use std;
println($"{1 < 2}");
println($"{'a' < 'b'}");
== and != work on the numeric types, string, bool and char, the types
that have a value to compare.
use std;
let name = "Ada";
println($"{1 == 1}");
println($"{name == "Ada"}");
println($"{true != false}");
println($"{'x' == 'x'}");
They do not work on a type you declare, on a union, on an
array, or on any. There is no structural comparison and no way to give a type
one.
pub type Point {
pub field x: i32;
}
let a = new Point { x: 1 };
let b = new Point { x: 1 };
let same = a == b;
To compare two values of your own type, write a method that says what comparing them means.
use std;
pub type Point {
pub field x: i32;
pub field y: i32;
}
impl Point {
pub fn equals(other: Point) -> bool {
return self.x == other.x && self.y == other.y;
}
}
println($"{new Point { x: 1, y: 2 }.equals(new Point { x: 1, y: 2 })}");
To ask which case a union holds, match
is what asks.
Logical
&&, || and ! work on bool and answer one. && and ||
short circuit; everything else
evaluates both sides, left before right, before the operator is applied.
use std;
let ready = true;
let done = false;
println($"{ready && !done}");
println($"{done || ready}");
Unary
| Operator | On | Produces |
|---|---|---|
! |
bool |
the negation |
- |
a number | the negation |
+ |
a number | the operand unchanged |
There is no negative literal, so -1 is this - applied to 1. That matters
only where it changes the reading: -a.b is -(a.b).
use std;
let value = 5;
println($"{-value}");
println($"{+value}");
println($"{!(value > 10)}");
Type operators
is asks what a value is and answers a bool.
as performs a conversion that is not implicit.
Both take a type on the right, and both bind looser than arithmetic.
use std;
let boxed: any = 32;
if boxed is i32 {
println($"{boxed as i32}");
}
Conditional
a ? b : c produces one of two values. The condition is a bool, and exactly
one of the two branches is evaluated.
use std;
let count = 3;
println(count == 1 ? "one" : "many");
println($"{count < 0 ? -count : count}");
It groups to the right, so a chain of them reads as a run of tests.
use std;
let count = 3;
let name = count == 0 ? "zero" : count == 1 ? "one" : "many";
println(name);
The two branches decide the type between them. Either they already agree, or one
of them converts implicitly to the other and that one is the type of the whole
expression. null brings no type of its own, so the other branch decides.
use std;
let wide: i64 = 1 > 0 ? 1 : 2;
let name = 1 > 0 ? "mew" : null;
println($"{wide} {name}");
Branches with nothing in common are an error.
let mixed = 1 > 0 ? 1 : "one";
Everything except assignment binds tighter, so a comparison in front of the ?
is the condition rather than part of it. Writing one as a statement is an error:
it produces a value and a statement discards it, so
if is what runs one of two statements.
use std;
let ready = true;
ready ? println("saved") : println("discarded");
Assignment
= and the compound operators are the loosest of all, and they group right to
left. Assignment is an expression that produces the value it assigned, which
Assignment covers.
What is not here
There is no bitwise operator, no shift, no increment or decrement, and no way to give a type an operator of its own. A method is how a type says what an operation on it means.