Control flow#
Choose between paths with if and when, and repeat work with loops that count, loops that wait for a condition, and loops that visit a list.
A program that only ran its statements from top to bottom could do very little. This chapter covers the statements that decide what runs next:
if,else ifandelsewhen, for choosing by valuerepeat ... times, for a known number of passesrepeat until, for looping on a conditiontraining arc, a condition loop that is guaranteed to endfor each, for visiting the elements of a list- counting loops: ranges,
repeat ... times with, and the C-stylefor while, for looping as long as a condition holdsbreakandcontinue
if#
if runs a block when a condition is true.
-- if / else if / else
task status(fuel: int) -> word {
if fuel > 75 {
give back "full"
} else if fuel > 25 {
give back "cruising"
} else if fuel > 0 {
give back "bingo fuel"
} else {
give back "dry"
}
}
task main() {
say status(90)
say status(40)
say status(10)
say status(0)
pilot fuel = 40
pilot hostile = true
if fuel > 25 and not hostile {
say "continue patrol"
} else {
say "return to base"
}
}
full
cruising
bingo fuel
dry
return to base
The condition comes straight after if, without parentheses. The block is always in braces, even when it holds a single statement:
task main() {
pilot fuel = 40
if fuel > 25 say "cruising"
}
error: expected '{', found 'say'
--> book_if_needs_braces.fk:3:18
|
3 | if fuel > 25 say "cruising"
| ^
error: unexpected end of file — missing '}'
--> book_if_needs_braces.fk:1:13
|
1 | task main() {
| ^ block opened here
An if can be followed by else, whose block runs when the condition is false. In between you can put any number of else if branches. The conditions are tested from the top, the first true one has its block run, and the rest are skipped. In status, a fuel level of 40 fails the first test, passes the second, and the remaining two branches are never looked at.
That order matters. Had the tests been written the other way round, with `fuel
0` first, every positive level would have been caught by it and reported as
bingo fuel.
The condition must be a bool#
Some languages treat zero as false and anything else as true. FREAK does not. A condition has to be a bool, and nothing else is accepted in its place:
task main() {
pilot fuel = 40
if fuel {
say "fuel remaining"
}
}
type error: if condition must have type bool, got int (line 3)
--> book_if_needs_bool.fk:3:1
3 | if fuel {
| ^
Say what you mean: if fuel > 0.
Comparisons cannot be chained the way they are in mathematics, either:
task main() {
pilot fuel = 40
if 25 < fuel < 75 {
say "cruising"
}
}
type error: operator '<' does not accept bool and int (line 3)
--> book_chained_comparison.fk:3:1
3 | if 25 < fuel < 75 {
| ^
The message makes sense once you see how the expression is read. 25 < fuel is worked out first and gives a bool. The compiler is then asked to compare that bool with 75. Write the two comparisons separately and join them: if 25 < fuel and fuel < 75.
if is a statement#
An if does something. It does not produce a value, so it cannot stand on the right of an =:
task main() {
pilot fuel = 40
pilot state = if fuel > 25 { "cruising" } else { "bingo" }
say state
}
error: unexpected 'if' — this token cannot start an expression
--> book_if_is_not_a_value.fk:3:19
|
3 | pilot state = if fuel > 25 { "cruising" } else { "bingo" }
| ^
error: unexpected 'else' — this token cannot start an expression
--> book_if_is_not_a_value.fk:3:47
|
3 | pilot state = if fuel > 25 { "cruising" } else { "bingo" }
| ^
There are two ways to get the same effect. Declare the pilot first and assign to it in each branch:
Illustrative snippet; not independently verified. Use the badged executable examples for tested programs.
pilot mut state = ""
if fuel > 25 {
state = "cruising"
} else {
state = "bingo"
}Or put the decision in a task and give the answer back, as status does in the first listing. The second way is usually cleaner, because the pilot that receives the answer is given its value once, and never needs a placeholder.
when#
A chain of else if that compares one value against several possibilities is better written as a when.
-- `when` picks one arm by comparing a value with literals.
task order_for(code: int) -> word {
when code {
1 -> give back "launch"
2 -> give back "hold"
3 -> give back "abort"
_ -> give back "unknown order"
}
}
task main() {
say order_for(1)
say order_for(9)
pilot rank = "ace"
when rank {
"rookie" -> say "simulator only"
"ace" -> {
say "cleared for sortie"
say "lead element"
}
_ -> say "report to the briefing room"
}
-- No arm matches and there is no `_`: nothing runs.
when rank {
"cadet" -> say "never printed"
}
}
launch
unknown order
cleared for sortie
lead element
when takes a value, called the subject, and a list of arms. Each arm is a literal value, an arrow, and one statement. The subject is compared with each arm's value in turn, and the first arm that is equal has its statement run. The arm written _ matches anything. It is the catch-all.
The listing shows the rest of the rules.
An arm holds one statement. To run several, put them in a block, as the "ace" arm does.
If no arm matches and there is no _, nothing happens, and the program carries on after the when. The compiler does not insist that you cover every case, so a missing arm fails silently. Unless you are certain the listed values are the only ones possible, end with _.
Arms are tried in order, so an arm after _ could never run, and neither could a second arm with a value already listed. The compiler refuses both:
task main() {
pilot rank = "ace"
when rank {
_ -> say "the catch-all comes first"
"ace" -> say "never reached"
}
}
type error: when arm is unreachable after '_' (line 5)
--> book_when_unreachable.fk:5:1
5 | "ace" -> say "never reached"
| ^
Keep the catch-all at the bottom.
The subject can be an int, a word, a bool or a num.
Arms are literals#
An arm's value has to be written out as a literal. It cannot be a pilot or a calculation:
task main() {
pilot code = 3
pilot abort_code = 3
when code {
abort_code -> say "abort"
_ -> say "continue"
}
}
type error: when case must be a literal or '_' (line 5)
--> book_when_needs_literal.fk:5:1
5 | abort_code -> say "abort"
| ^
When the values you are comparing against are not known until the program runs, use if and else if.
The literal also has to be of the subject's type:
task main() {
pilot code = 3
when code {
1 -> say "launch"
"abort" -> say "abort"
}
}
type error: when case type word does not match target type int (line 5)
--> book_when_wrong_type.fk:5:1
5 | "abort" -> say "abort"
| ^
when in a task that gives back#
Like if, a when is a statement and has no value. Like if, it pairs well with give back. In order_for, each arm gives back a word, and because the _ arm covers everything else, the compiler accepts that every path through the task gives something back.
repeat ... times#
The simplest loop runs a block a set number of times.
-- repeat N times
task main() {
repeat 3 times {
say "Valkyries, sound off"
}
-- A bare `repeat N times` has no counter. Keep one yourself, or write
-- `repeat N times with i` for one that starts at 0.
pilot mut number = 1
repeat 3 times {
say "Valkyrie-{number}"
number += 1
}
-- The count is worked out once, before the first pass.
pilot mut passes = 2
repeat passes times {
passes += 10
say "pass (passes is now {passes})"
}
-- Zero or a negative count means the body never runs.
repeat 0 times { say "never printed" }
}
Valkyries, sound off
Valkyries, sound off
Valkyries, sound off
Valkyrie-1
Valkyrie-2
Valkyrie-3
pass (passes is now 12)
pass (passes is now 22)
The count is any int: a literal, a pilot, a calculation. It is worked out once, before the first pass. The third loop in the listing changes passes inside its own body, and still makes exactly the two passes it started with.
The loop has no counter of its own. If the body needs to know which pass it is on, keep a pilot beside the loop and increase it yourself, as the second loop does, or let the loop count for you with repeat N times with i, shown later in this chapter.
A count of zero, or a negative count, means the body does not run at all.
The count must be an int. A num is refused even if its value is whole:
task main() {
repeat 2.5 times {
say "again"
}
}
type error: repeat count must have type int, got num (line 2)
--> book_repeat_needs_int.fk:2:1
2 | repeat 2.5 times {
| ^
repeat until#
When you do not know the number of passes in advance, loop on a condition.
-- repeat until CONDITION: the test comes first, and the loop stops
-- when the condition is true.
task main() {
pilot mut countdown = 3
repeat until countdown == 0 {
say countdown
countdown -= 1
}
say "launch"
-- Already true: the body does not run even once.
pilot mut laps = 0
repeat until true {
laps += 1
}
say laps
-- How many times can 1000 be halved before it reaches 1?
pilot mut value = 1000
pilot mut halvings = 0
repeat until value <= 1 {
value = value / 2
halvings += 1
}
say halvings
}
3
2
1
launch
0
9
Two things about repeat until are easy to get backwards.
The loop runs while the condition is false. until gives the condition for stopping. repeat until countdown == 0 keeps going as long as countdown is not zero. If you are used to while loops, this is the opposite sense. FREAK has a while as well, described below, so you can pick whichever reads better.
The condition is tested before each pass, including the first. If it is already true, the body never runs. The second loop in the listing shows this: laps is still zero afterwards.
For the loop to end, something in the body has to move the condition toward true. In the third loop, value is halved on every pass, so sooner or later it reaches 1.
A loop with no end#
Sometimes the natural place to stop is in the middle of the body. Then write a condition that is never true, and leave with break:
-- A loop with no natural end, left with `break`.
task main() {
pilot mut altitude = 0
repeat until false {
altitude += 2500
say "climbing through {altitude}"
if altitude >= 10000 { break }
}
say "level at {altitude}"
}
climbing through 2500
climbing through 5000
climbing through 7500
climbing through 10000
level at 10000
repeat until false loops forever by itself. The if inside decides when to stop. This is useful when the decision depends on something the body has just done, and you saw it in Chapter 2, where the game loop was left from the middle when the input ran out.
training arc#
A loop that depends on a condition can fail to end. If the body never makes the condition true, the program hangs. FREAK has a loop that rules this out.
-- training arc: a condition loop with a compulsory upper bound.
task main() {
-- Stops when the condition becomes true...
pilot mut power = 0
training arc until power >= 100 max 8 sessions {
power += 20
}
say power
-- ...or when the sessions run out, whichever comes first.
pilot mut stuck = 0
training arc until stuck > 1000 max 4 sessions {
stuck += 1
}
say stuck
}
100
4
A training arc is a repeat until with a limit. After max you give the largest number of passes, called sessions, that the loop may make. The loop ends when the condition becomes true or when the sessions are used up, whichever happens first.
In the first loop the condition wins: power reaches 100 on the fifth session and the loop stops with three sessions unused. In the second the limit wins: the condition would need more than a thousand sessions and the loop is allowed four.
Like repeat until, the condition is tested before each session. The limit can be any int expression.
After a training arc you often want to know which of the two ended it. Test the condition again:
-- collatz.fk: how many steps does a number take to reach 1?
-- Halve it when it is even; otherwise triple it and add one.
task collatz_steps(start: int) -> int {
pilot mut n = start
pilot mut steps = 0
training arc until n == 1 max 1000 sessions {
if n % 2 == 0 {
n = n / 2
} else {
n = 3 * n + 1
}
steps += 1
}
if n != 1 { give back -1 } -- ran out of sessions
give back steps
}
task main() {
pilot mut start = 1
pilot mut longest = 0
pilot mut longest_start = 1
repeat 30 times {
pilot steps = collatz_steps(start)
if steps > longest {
longest = steps
longest_start = start
}
start += 1
}
pilot six = collatz_steps(6)
pilot twenty_seven = collatz_steps(27)
say "6 takes {six} steps"
say "27 takes {twenty_seven} steps"
say "below 31, the longest chain starts at {longest_start} ({longest} steps)"
}
6 takes 8 steps
27 takes 111 steps
below 31, the longest chain starts at 27 (111 steps)
collatz_steps follows a famous rule: halve an even number, triple an odd one and add one, and count how many steps it takes to arrive at 1. Nobody has proved that every starting number gets there, so a plain repeat until n == 1 would be a loop with no guarantee. The training arc gives it one. If a thousand sessions pass without reaching 1, the loop ends anyway, the test after it sees that n is not 1, and the task gives back -1 to say that it gave up.
Use a training arc whenever a loop's end depends on something you cannot fully control: a search that might not converge, a retry that might never succeed, input that might never be valid.
for each#
The last loop visits the elements of a list, one after another. Lists are the subject of a chapter in Part II. For now it is enough to know that square brackets around values separated by commas make one.
-- for each: visit every element of a list, in order.
task main() {
pilot squadron = ["Takeru", "Meiya", "Sumika"]
for each name in squadron {
say "{name}, report in"
}
-- The loop gives you the element, not its position. Count it yourself.
pilot mut position = 1
for each name in squadron {
say "{position}. {name}"
position += 1
}
-- break and continue work here too.
pilot readings = [12, 0, 7, 0, 30, 99]
pilot mut total = 0
for each reading in readings {
if reading == 0 { continue }
if reading > 50 { break }
total += reading
}
say total
}
Takeru, report in
Meiya, report in
Sumika, report in
1. Takeru
2. Meiya
3. Sumika
49
After for each comes a name, then in, then the list. On each pass the name holds the next element. It exists only inside the body, like any pilot declared in a block.
for each gives you the element and not its position. When you need both, count alongside the loop, as the second loop in the listing does.
The thing after in has to be a list or a range. A word is not a list of its letters:
task main() {
for each letter in "Takeru" {
say letter
}
}
type error: for each requires a list with a known element type (line 2)
--> book_for_each_word.fk:2:1
2 | for each letter in "Takeru" {
| ^
Counting loops#
When you want the numbers themselves, loop over a range, or use a loop that counts.
-- Counting loops. A range a..b stops before b; a..=b includes b.
task main() {
for each i in 1..4 {
say "range {i}"
}
for each i in 1..=3 {
say "inclusive {i}"
}
for each i in 0..10 step 4 {
say "step {i}"
}
repeat 2 times with pass {
say "pass {pass}"
}
for (pilot i = 3; i > 0; i -= 1) {
say "countdown {i}"
}
}
range 1
range 2
range 3
inclusive 1
inclusive 2
inclusive 3
step 0
step 4
step 8
pass 0
pass 1
countdown 3
countdown 2
countdown 1
There are three forms, and they mean the same as they would in most languages. Use whichever reads best for the loop at hand.
for each i in 1..4givesithe values 1, 2 and 3. The end of a range written..is not included. Written..=, as in1..=3, it is.stepsets the distance between values:0..10 step 4gives 0, 4 and 8.repeat 2 times with passis arepeat ... timesloop that names its counter. The counter starts at 0.for (pilot i = 3; i > 0; i -= 1)is the form from C and its relatives: a pilot to start with, a condition tested before each pass, and a statement run after each pass.
In every form the counter is a pilot that exists only inside the loop.
while#
while is repeat until turned the other way round: it runs as long as its condition is true. repeat while is another spelling of the same loop.
-- `while` runs as long as its condition is true: the opposite of `repeat until`.
task main() {
pilot fuel = 3
while fuel > 0 {
say "fuel {fuel}"
fuel -= 1
}
pilot laps = 0
repeat while laps < 2 {
laps += 1
}
say "laps {laps}"
}
fuel 3
fuel 2
fuel 1
laps 2
Like repeat until, while tests its condition before each pass, so a condition that is false at the start means the body never runs. while is a reserved word, so it cannot be used as a name.
break and continue#
Two statements change the course of any loop from inside its body.
-- break leaves the loop; continue skips to its next pass.
task main() {
pilot mut n = 0
repeat 10 times {
n += 1
if n % 2 == 0 { continue } -- skip the even numbers
if n > 7 { break } -- and stop after 7
say n
}
say "stopped at {n}"
-- In nested loops, both act on the innermost loop only.
pilot mut row = 1
repeat 3 times {
pilot mut column = 1
repeat 3 times {
if column > row { break }
say "{row},{column}"
column += 1
}
row += 1
}
}
1
3
5
7
stopped at 9
1,1
2,1
2,2
3,1
3,2
3,3
break ends the loop. Execution carries on with the first statement after it.
continue ends the current pass. In a repeat ... times loop the next pass begins; in a repeat until, a while or a training arc the condition is tested again; in a for each the loop moves to the next element or value.
When loops are nested, break and continue act on the innermost loop that contains them. The second half of the listing prints a triangle: the inner loop is cut short by break when column passes row, and the outer loop carries on to the next row. There is no way to break out of two loops at once. If you need to, put the loops in a task and use give back, which leaves the whole task from any depth.
Be careful with continue in a loop that keeps its own counter. If the counter is increased at the bottom of the body, continue skips the increase. In the listing, n += 1 is the first statement of the body for exactly this reason.
Putting it together#
Here is a small program that uses a counted loop, a task that decides with if, and the conversion from Chapter 3.
-- fizzbuzz.fk: count from 1 to 15. Multiples of 3 say Fizz, multiples of 5
-- say Buzz, and multiples of both say FizzBuzz.
task label(n: int) -> word {
if n % 15 == 0 { give back "FizzBuzz" }
if n % 3 == 0 { give back "Fizz" }
if n % 5 == 0 { give back "Buzz" }
give back n.to_word()
}
task main() {
pilot mut n = 1
repeat 15 times {
say label(n)
n += 1
}
}
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
label tests the most specific case first. A multiple of 15 is also a multiple of 3 and of 5, so if those tests came first, FizzBuzz would never be given back. Order matters in a chain of tests, as it did at the start of this chapter.
The task gives back a word in every case, so the number has to be converted with to_word(). All four give back statements must agree on the type.
In V4#
At the pinned commit V4 runs if, when on ints, repeat ... times, repeat until, break and continue. These two listings give the same results on both compilers:
-- Three of V3's loops, with break and continue, on V4.
task main() -> int {
pilot total = 0
repeat 4 times { total += 10 } -- 40
pilot n = 0
repeat until n >= 6 {
n += 1
if n == 2 { continue } -- skips 2
if n == 5 { break } -- stops before 5
total += n -- adds 1, 3, 4
}
pilot drills = 0
training arc until drills >= 3 max 10 sessions { drills += 1 }
give back total + drills -- 40 + 8 + 3
}
task order_code(code: int) -> int {
pilot answer = 0
when code {
1 -> answer = 10
2 -> answer = 20
_ -> answer = 99
}
give back answer
}
task main() -> int {
give back order_code(2) + order_code(7)
}
It checks conditions as strictly as V3:
task main() -> int {
pilot fuel = 40
if fuel { give back 1 }
give back 0
}
2|0@45:68|if condition must be bool|got int
The rest of this section is what does not match.
A chain in which every branch gives back does not build. This is the shape of status in this chapter's first listing: an if, its else if branches and an else, each ending in give back.
-- Every branch gives back, so nothing follows the chain.
task sign(n: int) -> int {
if n > 0 {
give back 1
} else if n < 0 {
give back 2
} else {
give back 3
}
}
task main() -> int {
give back sign(5)
}
2|0@58:202|mir cfg unreachable live block|sign block 5 has no predecessors but still carries live statements or control flow
The message comes from inside the compiler, and it is a defect in this commit, not a rule of the language. Until it is fixed, let the last case follow the chain instead of sitting in an else:
-- The same decision, with the last case after the chain.
task sign(n: int) -> int {
if n > 0 {
give back 1
} else if n < 0 {
give back 2
}
give back 3
}
task main() -> int {
give back sign(5) * 100 + sign(0 - 5) * 10 + sign(0)
}
**An arm after _ is an error.** V3 refuses it, as shown earlier in this chapter. V4 refuses the program too, with its own message:
task main() -> int {
pilot code = 0
pilot rank = 5
when rank {
_ -> code += 2
5 -> code += 4
}
give back code
}
2|0@106:120|unreachable when arm|an earlier arm already matches every remaining value
2|0@0:147|mir cfg unreachable live block|main block 3 has no predecessors but still carries live statements or control flow
The first line is the useful one. The second is the compiler tripping over the same arm again.
**for each is understood and cannot be built yet.** V4 reads the loop and checks it. The stage that generates code has no lists yet:
task main() -> int {
pilot total = 0
for each reading in [12, 7, 30] {
total += reading
}
give back total
}
v4-native-contract-error=native rvalue not yet supported: List
**The session limit of a training arc is not enforced.**
-- The condition here needs eleven passes; the cap allows three.
-- V3 stops at 3. This V4 snapshot checks the cap's type but does not
-- enforce it, so the loop runs until the condition is true.
task main() -> int {
pilot drills = 0
training arc until drills > 10 max 3 sessions {
drills += 1
}
give back drills
}
This is the serious one, because nothing reports it. V3 stops this loop after three sessions and gives 3. At the pinned commit V4 checks that the limit is a number and then builds an ordinary condition loop, which runs until drills exceeds 10. A training arc whose condition can never become true does not end in this V4 commit. That is the one situation the loop exists to prevent.
**V4 has with growth.** The specification lets a training arc heading end with with growth, which asks the compiler to verify that the body really does change the pilot named in the condition. V3 does not accept it. This V4 commit does:
-- `with growth`: the body must change the pilot the condition tests.
task main() -> int {
pilot power = 0
training arc until power >= 100 max 8 sessions with growth {
power += 20
}
give back power
}
And it refuses a loop whose body cannot make progress:
task main() -> int {
pilot power = 0
pilot fatigue = 0
training arc until power >= 100 max 8 sessions with growth {
fatigue += 20
}
give back fatigue
}
2|0@67:155|training arc with growth must mutate condition subject|Yuuko found no progress for power
The name in that message, Yuuko, is one of the voices the specification gives to the compiler's diagnostics. Part VII covers them.
Planned#
In the specification, when is much more than a comparison with literals. Its arms are patterns that can take a value apart: match one variant of a type with several forms and name the fields inside it, in one step. The compiler is also to check that the arms cover every possibility. That depends on variants, which V3 does not have. Part VII covers both.
The specification lets for each deliver the position along with the element, written for each (i, item) in list.enumerate(). Until a compiler has it, count with a pilot beside the loop.
Summary#
if chooses on a condition, which must be a bool. else if and else extend it, and the first true branch wins. when chooses by comparing one value with literal arms; put _ last to catch everything else. Both are statements, not values.
repeat N times runs a fixed number of passes, counted once at the start, and can name its counter with with. repeat until runs while its condition is false and while runs while it is true; both test before each pass. training arc is repeat until with a compulsory limit on the number of passes, so it always ends. for each visits each element of a list or each value of a range, and the C-style for counts with a start, a condition and a step.
break leaves the innermost loop and continue skips to its next pass.
That completes the core of the language. With pilots, tasks and control flow you can write any computation. What you cannot do yet is hold more than one value at a time, or describe data of your own. Part II begins with the type you have used most and understood least: the word.