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 anifmarks its block, so itselseis the unlikely side;[Unlikely]marks the block as the rare one. To mark anelse, mark theifthe other way. -
In an
else if, the attribute goes betweenelseandif:} else [Unlikely] if (x) {. -
A hint is an LLVM branch weight (2000 to 1, as clang gives
__builtin_expect), so it matters with optimization (-O1and 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.