Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Control flow

Pepper has the familiar structured control flow of C-family languages: if and else to choose, while and for to repeat, and break, continue and return to leave early.

if and else

if runs a block when its condition is true, and the optional else block when it is false. The parentheses around the condition are optional; the braces around each block are required, even for a single statement.

# Branching with if and else.
classify(n: int): String {
    if n < 0 {
        return "negative";
    } else if n == 0 {
        return "zero";
    } else {
        return "positive";
    }
}

main(): int {
    var temperature = 25;

    # Parentheses around the condition are optional; braces are required.
    if temperature > 30 {
        print("hot");
    } else {
        print("not hot");
    }

    if (temperature > 20) {
        temperature = 20;
    }
    assert(temperature == 20, "clamped to 20");

    assert(classify(-5) == "negative", "negative");
    assert(classify(0) == "zero", "zero");
    assert(classify(7) == "positive", "positive");

    # For a choice between two values, the ternary operator is shorter.
    var parity = temperature % 2 == 0 ? "even" : "odd";
    assert(parity == "even", "20 is even");

    # && and || stop at the first operand that decides the result, so the
    # count check guards the index.
    var readings = Array<int>();
    var starts_high = false;
    if (readings.count > 0 && readings[0] > 100) {
        starts_high = true;
    }
    assert(!starts_high, "an empty array does not start high");
    return 0;
}

The condition is usually a bool, but a number also works: any value other than zero counts as true, as in C. A raw pointer (__ptr<T>) or a function value counts as true when it is not null. A struct counts only if it declares the truth-test operator !!(): bool, which the condition calls; any other struct is an error, so write node !== __null to ask whether a reference is set (see Truth tests and conditions). The same holds for the condition of while and of ?:, the operands of &&, ||, and and or, and the operand of !.

Chains of conditions

else if tests another condition when the ones before it were false. The first true condition’s block runs, and the final else block, if there is one, runs when none was true:

if x > 0 {
    return 1;
} else if x < 0 {
    return -1;
} else {
    return 0;
}

For a choice between two values, the conditional operator condition ? a : b is often shorter than an if.

Guarding a check

&& and || short-circuit, as in C: the right operand is evaluated only when the left one does not already decide the result. A guard can therefore come first in the same condition:

if items.count > 0 && items[0] > 100 {
    print("starts high");
}

When items is empty, items[0] is never read.

while

while repeats its block as long as the condition is true. The condition is checked before each iteration, so the body may run zero times. As with if, the parentheses are optional and the braces are required.

var countdown = 3;
while countdown > 0 {
    countdown -= 1;
}

for

for runs its block once for each element of an Array, each code point of a String, or each value of any type that follows the iterator protocol:

for (var score in scores) {
    total += score;
}

The loop variable can be declared with var or a type (for (int score in scores), for (var score: int in scores)), and the parentheses are optional. Iteration covers the protocol behind for and how to make your own types iterable.

Pepper has no C-style for (init; condition; step) loop. To count, loop over a range: for (var i in 0..count).

with

with enters a scope object for the length of a block and binds what it hands out. The object cleans up when it is destroyed, on every way out of the block:

with (var session = Session("grace")) {
    greet(session.user);
}

Scoped blocks with with covers the protocol and the optional else block.

match

match runs the arm for the case an enum value holds, and the compiler checks that every case is handled:

match (light) {
    Red, Amber => { stop(); }
    Green => { go(); }
}

Enums and match covers patterns and the else arm.

Branch hints: [Likely] and [Unlikely]

[Likely] or [Unlikely] on the line above an if, an else if’s if or a match arm (the else arm too) says how often that branch runs. The optimizer keeps the likely path as straight-line code and moves the unlikely one, such as error handling, out of its way. A hint never changes what the program computes; a wrong one costs only speed.

# Branch hints: `[Likely]` and `[Unlikely]` say which way a branch usually
# goes, so the optimizer keeps the common path straight.
enum Reading {
    Value(celsius: double),
    Missing,
}

parse_digit(c: char): int {
    [Unlikely]
    if (c < '0' || c > '9') {
        return -1;
    } else [Likely] if (c != '0') {
        return c - '0';
    }
    return 0;
}

describe(reading: Reading): String {
    match (reading) {
        [Likely]
        Value(celsius) => {
            return $"{celsius}";
        }
        [Unlikely]
        Missing => {
            return "missing";
        }
    }
}

main(): int {
    assert(parse_digit('7') == 7, "a digit");
    assert(parse_digit('x') == -1, "not a digit");
    assert(describe(Reading.Missing) == "missing", "the rare arm still runs");
    return 0;
}
  • [Likely] above an if marks its block, so its else is the unlikely side; [Unlikely] marks the block as the rare one. To mark an else, mark the if the other way.

  • In an else if, the attribute goes between else and if: } else [Unlikely] if (x) {.

  • A hint is an LLVM branch weight (2000 to 1, as clang gives __builtin_expect), so it matters with optimization (-O1 and up).

  • Only one of them, with no arguments, can mark a branch, and no other attribute can:

    main.pep:3:5: error: 'Target' cannot mark an 'if'; only '[Likely]' or '[Unlikely]' can
        3 |     [Target(OS(Linux))]
          |     ^~~~~~~~~~~~~~~~~~~
    

while loops and the arms of a match expression take no hint yet.

break and continue

break leaves the innermost loop immediately. continue skips the rest of the current iteration and goes on with the next one; in a while loop that means checking the condition again, so update the counter before any continue.

# Looping with while and for, and leaving early with break and continue.
main(): int {
    # while repeats as long as its condition is true.
    var countdown = 3;
    var launched = false;
    while countdown > 0 {
        countdown -= 1;
    }
    launched = countdown == 0;
    assert(launched, "counted down to zero");

    # for visits each element of an Array (or any other iterable).
    var scores = Array<int>();
    scores.append(70);
    scores.append(85);
    scores.append(92);
    var total = 0;
    for (var score in scores) {
        total += score;
    }
    assert(total == 247, "sum of the scores");

    # continue skips to the next iteration; break leaves the loop.
    var odd_sum = 0;
    var i = 0;
    while (i < 100) {
        i += 1;
        if (i % 2 == 0) { continue; }
        if (i > 9) { break; }
        odd_sum += i;
    }
    assert(odd_sum == 25, "1 + 3 + 5 + 7 + 9");

    # break and continue apply to the innermost loop.
    var pairs = 0;
    for (var a in scores) {
        for (var b in scores) {
            if (b == a) { break; }
            pairs += 1;
        }
    }
    assert(pairs == 3, "(85, 70), (92, 70), (92, 85)");
    return 0;
}

Both work in while and for loops and always apply to the innermost loop. There are no labeled breaks: to leave several loops at once, set a flag and test it, or move the loops into a function and return from it.

return

return value; leaves the current function and hands value back to the caller. It works from anywhere in the function, including from inside loops. See Functions.

In a function that returns nothing, a bare return; leaves the function early.