Operators · Lesson 2.5

Precedence

Source
Predict which operator applies first, how same-rank operators group left to right, and when to add parentheses.

2 + 3 * 4 could mean 20 or 14, depending on which operator goes first. Maths settled this long ago — multiplication before addition — and Rux follows the same idea for every operator it has. The rules that decide the order are called precedence, and knowing the few that matter lets you read an expression the way the compiler does.

The ranking

From tightest to loosest, the operators of this part rank like this:

RankOperatorsKind
tightestx++ x--step after reading
-x !x ++x --xunary
asconversion
* / %multiplication
+ -addition
< <= > >=ordering
== !=equality
&&and
||or
loosest= += -= …assignment

A tighter operator grabs its operands first. Parentheses override all of it.

PrintLine("2 + 3 * 4    = {}", 2 + 3 * 4);
PrintLine("(2 + 3) * 4  = {}", (2 + 3) * 4);

* ranks above +, so 3 * 4 happens first and the answer is 14. Wrap 2 + 3 in parentheses and it is 20. % ranks with *, not with +, so 7 + 10 % 4 is 7 + 2, which is 9.

Same rank, left to right

When operators share a rank, they group from the left:

PrintLine("100 / 10 / 5   = {}", 100 / 10 / 5);
PrintLine("100 / (10 / 5) = {}", 100 / (10 / 5));
PrintLine("10 - 3 - 2     = {}", 10 - 3 - 2);

100 / 10 / 5 is (100 / 10) / 5, which is 2. Grouping it the other way gives 50. For + and * the grouping never changes the answer, but for - and / it does.

as converts only its neighbour

as binds tighter than any arithmetic, so it converts the single operand right next to it — not the whole expression on its left:

let a: int32 = 17;
let b: int32 = 5;
PrintLine("(a / b) as float64            = {}", (a / b) as float64);
PrintLine("a as float64 / b as float64   = {}", a as float64 / b as float64);

The first line divides two integers, losing the fraction, and only then converts: 3.0. The second converts each integer first and divides two floats: 3.4. Here is how the compiler groups both:

flowchart LR
    e1["(a / b) as float64"] --> d1["a / b = 3<br/>integer division"] --> c1["3 as float64<br/>= 3.0"]
    e2["a as float64 / b as float64"] --> c2["17.0 and 5.0<br/>converted first"] --> d2["17.0 / 5.0<br/>= 3.4"]

Writing a / b as float64 would convert only b, and then / would have an int32 on one side and a float64 on the other — which the compiler refuses.

Conditions read naturally

Arithmetic ranks above comparison, and comparison above && and ||. That order is chosen so that the usual conditions need no parentheses at all:

let age: int32 = 25;
PrintLine("age >= 18 && age < 65   is {}", age >= 18 && age < 65);

The comparisons are worked out first, then && combines their answers. Between the two logical operators, && binds tighter than ||, the same way * binds tighter than +:

PrintLine("true || false && false    is {}", true || false && false);
PrintLine("(true || false) && false  is {}", (true || false) && false);

The first is true || (false && false), which is true. And ! applies only to the operand right next to it, so !ready && busy is (!ready) && busy — use parentheses, !(ready && busy), to negate the whole thing.

When in doubt, add parentheses

Precedence decides what the compiler does; parentheses decide what the reader sees. a * b + c is clear to anyone, but x || y && z makes most people stop and think. Parentheses that match the default grouping change nothing in the program and save the next reader that pause.

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// When an expression mixes operators, precedence decides which applies first,
// the way multiplication comes before addition in maths. From tightest to
// loosest, the operators seen so far rank like this:
//
//     x++  x--          step after reading
//     -x  !x  ++x  --x  unary
//     as                conversion
//     *  /  %           multiplication
//     +  -              addition
//     <  <=  >  >=      ordering
//     ==  !=            equality
//     &&                and
//     ||                or
//     =  +=  -=  ...    assignment, loosest of all
//
// Operators on the same rank group from left to right. Parentheses override
// all of it, and are worth adding wherever a reader would have to stop and think.
import Io::PrintLine;

func Main() -> int {
    // Multiplication before addition, unless parentheses say otherwise.
    PrintLine("2 + 3 * 4    = {}", 2 + 3 * 4);
    PrintLine("(2 + 3) * 4  = {}", (2 + 3) * 4);

    // `%` ranks with `*`, not with `+`.
    PrintLine("7 + 10 % 4   = {}", 7 + 10 % 4);

    // Same rank, left to right: 100 / 10 first, then / 5. Grouping the other
    // way round gives a different answer.
    PrintLine("100 / 10 / 5   = {}", 100 / 10 / 5);
    PrintLine("100 / (10 / 5) = {}", 100 / (10 / 5));
    PrintLine("10 - 3 - 2     = {}", 10 - 3 - 2);

    // `as` binds tighter than arithmetic, so it converts only its neighbour.
    // Converting the result of an integer division is too late: the fraction
    // was already gone.
    let a: int32 = 17;
    let b: int32 = 5;
    PrintLine("(a / b) as float64            = {}", (a / b) as float64);
    PrintLine("a as float64 / b as float64   = {}", a as float64 / b as float64);

    // Arithmetic happens before comparing, and comparing before `&&`, so a
    // range check needs no parentheses at all.
    let age: int32 = 25;
    PrintLine("age >= 18 && age < 65   is {}", age >= 18 && age < 65);

    // `&&` binds tighter than `||`, the same way `*` binds tighter than `+`.
    PrintLine("true || false && false    is {}", true || false && false);
    PrintLine("(true || false) && false  is {}", (true || false) && false);

    // `!` applies only to the operand right next to it.
    let ready = true;
    let busy = false;
    PrintLine("!ready && busy    is {}", !ready && busy);
    PrintLine("!(ready && busy)  is {}", !(ready && busy));
    return 0;
}

Run it

cd Examples/Operators/Precedence
rux run
2 + 3 * 4    = 14
(2 + 3) * 4  = 20
7 + 10 % 4   = 9
100 / 10 / 5   = 2
100 / (10 / 5) = 50
10 - 3 - 2     = 5
(a / b) as float64            = 3.0
a as float64 / b as float64   = 3.4
age >= 18 && age < 65   is true
true || false && false    is true
(true || false) && false  is false
!ready && busy    is false
!(ready && busy)  is true

Common mistakes

Converting too late.
(a / b) as float64 is 3.0, not 3.4: the integer division has already dropped the fraction. Convert each operand before dividing.
Expecting as to cover the whole expression.
a / b as float64 converts only b, and fails with error: operator '/' cannot combine left operand 'int32' with right operand 'float64'.
Negating more than the next operand.
!count == 0 is (!count) == 0, and when count is an int32 it fails with error: operator '!' requires a bool operand, but found 'int32'. Write count != 0, or wrap what you mean in parentheses.

Try it yourself

  1. Predict, then print, 2 * 3 + 4 * 5, 20 - 4 - 6 and 8 / 2 * 4.
  2. Add parentheses to true || false && false that do not change its value, and then some that do.
  3. Write the condition "age is under 13 or over 65, and has a ticket" with an int32 age and a bool ticket. Which parentheses are required?

Learn more