# Chapter 4: Control Flow

**What you'll learn:** branching with `if` (as a statement and as an expression) and a first look at `match`; looping with `while`, `do`/`while`, counted `for` and `for`-in over collections and ranges; `break`, `continue` and labels; destructuring; and running cleanup code with `try`/`finally`, `with` and `del`.

## `if`, `else if`, `else`

```loup
@mode(host.scripted)

let temperature = 72;

if (temperature > 90) {
    println("hot");
} else if (temperature > 70) {
    println("pleasant");
} else if (temperature > 50) {
    println("cool");
} else {
    println("cold");
}
```

*Listing 4-1: A chain of conditions*

```text
pleasant
```

Two rules apply to every `if`: the condition is in parentheses, and it must be a `bool` (Chapter 2 showed the error for `if (count)`). The branches are always blocks in braces.

### Mistake: leaving out the parentheses

Python, Go, Rust and Swift write conditions without parentheses. Loup does not:

```loup,does_not_compile
@mode(host.scripted)

let temperature = 72;
if temperature > 90 {
    println("hot");
}
```

```text
main.lpscr:4:4: error[E0001]: E_CONDITION_PARENS: the condition of `if` must be in parentheses; add them: `if (...) {`
```

The same rule, with the same message, applies to `while (…)` and to the header of `for (…)`. Add the parentheses: `if (temperature > 90) {`.

### `if` as an expression

`if` produces a value when both branches end in an expression. Use it where other languages use the ternary operator `?:`, which Loup does not have:

```loup
#~ @mode(host.scripted)
let count = 1;
let noun = if (count == 1) { "file" } else { "files" };
println(i"{$ count $} {$ noun $} changed");
```

```text
1 file changed
```

### Blocks are scopes

A binding declared inside a block is visible only inside it, and may shadow an outer binding of the same name:

```loup
#~ @mode(host.scripted)
let level = 1;
if (level > 0) {
    let level = 2;
    let note = "inner";
    println(i"{$ note $}: {$ level $}");
}
println(level);
```

```text
inner: 2
1
```

Using `note` after the `if` would be `undefined identifier`. Declaring the same name twice in one scope is `E_REDECLARED`.

## Loops

### `while` and `do`/`while`

`while` checks its condition before each pass; `do { … } while (…)` checks after, so the body runs at least once:

```loup
@mode(host.scripted)

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

var attempts = 0;
do {
    attempts = attempts + 1;
    println(i"attempt {$ attempts $}");
} while (attempts < 2);
```

```text
3
2
1
attempt 1
attempt 2
```

### `for`-in over collections and ranges

`for (x in items)` visits each element of a list, the keys of a dict, or the values of a generator. `start..end` counts from `start` up to, but not including, `end`:

```loup
@mode(host.scripted)

let fruits = ["apple", "banana", "cherry"];
for (fruit in fruits) {
    println(fruit);
}

for (i in 0..fruits.length()) {
    println(i"{$ i + 1 $}. {$ fruits[i] $}");
}

let ages = {"alice": 30, "bob": 25};
for ((name, age) in ages.entries()) {
    println(i"{$ name $} is {$ age $}");
}
```

*Listing 4-2: Iterating a list, a range and a dict*

```text
apple
banana
cherry
1. apple
2. banana
3. cherry
alice is 30
bob is 25
```

`ages.entries()` yields `(key, value)` tuples, and `(name, age)` unpacks each one. Iterating the dict itself, `for (name in ages)`, visits the keys in insertion order.

> **Note.** Some older Loup material uses `range(1, 6)` and `list.enumerate()`. Neither exists in Loup 1.25; use `1..6` and an index range as above.

### Mistake: a range as a value

`0..n` is part of the loop syntax, not a value:

```loup,does_not_compile
@mode(host.scripted)

let indexes = 0..5;
for (i in indexes) {
    println(i);
}
```

```text
main.lpscr:3:15: error[E0001]: E_RANGE_FORM_MISPLACED: the range form `start..end` is permitted only as the iteration source of a `for` loop or a comprehension clause (§1-6.4)
```

Write the range in the loop header, `for (i in 0..5)`, or build a list with a comprehension, `let indexes = [i for i in 0..5];`. There is no inclusive form (`0..=5`) and no step; use a `while` loop when you need either.

### Mistake: assigning to the loop variable

The variable of a `for`-in loop is immutable, like a `let`:

```loup,does_not_compile
@mode(host.scripted)

for (price in [10, 20, 30]) {
    price = price * 2;
    println(price);
}
```

```text
main.lpscr:4:5: error[E0001]: cannot assign to immutable binding 'price'
```

If you want a changeable copy, declare it with `var`. Assigning it changes only that iteration's copy, never the list:

```loup
@mode(host.scripted)

for (var price in [10, 20, 30]) {
    price = price * 2;
    println(price);
}
```

```text
20
40
60
```

### Counted `for`

The three-part loop from C is available too. Its counter must be a `var`, because the update clause assigns it:

```loup
#~ @mode(host.scripted)
for (var i = 0; i < 3; i = i + 1) {
    println(i);
}
```

```text
0
1
2
```

The Language Reference writes this loop with `let i = 0` (§1-6.4); the 1.25 compiler rejects that with `cannot assign to immutable binding 'i'`, so use `var`. For simple counting, `for (i in 0..3)` is shorter.

> **Caution.** Do not add elements to a list while a `for` loop is iterating it. The Language Reference says a script gets a warning; in Loup 1.25 the loop keeps visiting the new elements, so `for (x in items) { items.push(x); }` never ends. Build a new list instead.

### `break`, `continue` and labels

`break` leaves the innermost loop and `continue` skips to its next pass. To leave an outer loop from inside an inner one, label the outer loop:

```loup
@mode(host.scripted)

let grid = [[1, 2, 3], [4, -5, 6], [7, 8, 9]];

search: for (r in 0..grid.length()) {
    for (c in 0..grid[r].length()) {
        if (grid[r][c] < 0) {
            println(i"first negative at row {$ r $}, column {$ c $}");
            break search;
        }
    }
}
```

*Listing 4-3: Breaking out of two loops with a label*

```text
first negative at row 1, column 1
```

`continue search;` would move on to the next row instead. A `break` or `continue` naming a label that does not exist is a compile error (`break label 'nowhere' not found`).

### Comprehensions

A list comprehension builds a list from a loop in one expression, optionally with a filter:

```loup
#~ @mode(host.scripted)
let squares = [n * n for n in 0..6];
let evenSquares = [n * n for n in 0..6 if n % 2 == 0];
println(squares);
println(evenSquares);
```

```text
[0, 1, 4, 9, 16, 25]
[0, 4, 16]
```

Note that a comprehension's `for` clause has no parentheses. Chapter 5 shows comprehensions over other collections.

## Destructuring

A `let` or `var` can unpack a tuple or a list into several bindings, including nested ones:

```loup
#~ @mode(host.scripted)
let (width, height) = (1920, 1080);
println(width * height);

let (name, (lat, lon)) = ("Lisbon", (38.7, -9.1));
println(i"{$ name $}: {$ lat $}, {$ lon $}");

let [first, second] = ["gold", "silver"];
println(first);
```

```text
2073600
Lisbon: 38.7, -9.1
gold
```

> ⚠ **Planned.** Rest patterns (`let (first, *rest) = …`) and struct patterns (`let Point { x, y } = p;`) are in the Language Reference (§1-6.8) but are not accepted by the 1.25 parser. Inside `match`, struct and enum patterns do work (Chapter 7).

## A First Look at `match`

`match` compares a value against a list of patterns and evaluates the arm of the first one that fits. Chapter 7 covers it in full; here it is on strings:

```loup
@mode(host.scripted)

func describe(status: string): string {
    return match status {
        "active" => "user is active",
        "inactive" => "user is inactive",
        _ => i"unknown status: {$ status $}",
    };
}

println(describe("active"));
println(describe("banned"));
```

```text
user is active
unknown status: banned
```

`_` matches anything. Leave it out and the program still compiles — a string can hold any value, so the compiler cannot check that you covered them all — but a value no arm matches throws at run time:

```loup,throws
@mode(host.scripted)

let status = "banned";
let message = match status {
    "active" => "user is active",
    "inactive" => "user is inactive",
};
println(message);
```

```text
Uncaught MatchError: No matching arm for value in match expression
  at <main> (main.lpscr:4:15)
```

Give every `match` on a string or a number a `_` arm. For enums, the compiler does check that every variant is covered (Chapter 7).

## Cleanup That Always Runs

Scripts open files, start processes and create temporary state that must be cleaned up even when something fails. Loup has three tools for this.

### `try`/`finally`

A `finally` block runs when its `try` block is left in any way — falling off the end, `return`, `break`, `continue` or an exception:

```loup
@mode(host.scripted)

func loadConfig(path: string): string {
    println(i"opening {$ path $}");
    try {
        return readText(path);
    } finally {
        println(i"done with {$ path $}");
    }
}

try {
    println(loadConfig("missing.toml"));
} catch (IoError as e) {
    println(i"could not load: {$ e.message $}");
}
```

*Listing 4-4: finally runs even when the read fails*

```text
opening missing.toml
done with missing.toml
could not load: read_text failed on 'missing.toml': No such file or directory (os error 2)
```

`catch (IoError as e)` handles one type of exception and binds it to `e`. Chapter 10 covers exceptions in full.

### `with`

`with (name = expression) { … }` acquires a resource, runs the block and releases the resource when the block ends, however it ends. The resource's class must implement `Closeable` (it then needs a `close()` method) or the more general `Scoped[T]` (§1-6.6). Several resources are released in reverse order:

```loup
@mode(host.scripted)

class LogFile implements Closeable {
    pub let name: string;

    new(name: string) {
        this.name = name;
        println(i"open {$ name $}");
    }

    pub func close(): void {
        println(i"close {$ this.name $}");
    }
}

try {
    with (input = LogFile("in.log"), output = LogFile("out.log")) {
        println(i"copying {$ input.name $} to {$ output.name $}");
        throw RuntimeError("disk full");
    }
} catch (RuntimeError as e) {
    println(i"failed: {$ e.message $}");
}
```

*Listing 4-5: with closes both resources, in reverse order, when the block throws*

```text
open in.log
open out.log
copying in.log to out.log
close out.log
close in.log
failed: disk full
```

A class that implements neither interface is rejected with `E_WITH_NOT_SCOPED`, and the message reminds you that `Closeable` and `Releasable` provide what `with` needs.

### `del`

`del name;` ends a binding early and calls the object's `finalize()` method, if it has one, at that point (§1-6.11). It is rarely needed; use it when a large object should be released before the end of its scope.

### Known gap: `defer`

The Language Reference defines `defer` (§1-6.5): `defer close(handle);` registers a statement to run when the enclosing block ends, and several `defer`s run last-registered-first. The 1.25 compiler accepts `defer`, but the interpreter never runs the deferred statement:

```loup,not_desired_behavior
@mode(host.scripted)

func work(): void {
    defer println("cleanup");
    println("working");
}

work();
println("after");
```

```text
working
after
```

`cleanup` is never printed. Until this is fixed, use `try`/`finally` or `with` for cleanup that must happen.

## A Worked Example: Guessing by Halving

This script guesses a secret number between 1 and 100 by halving the range on every try. It uses `while (true)` with `break`, an `else if` chain, an `if` expression and the main guard from Chapter 1:

```loup
@mode(host.scripted)

## Find `secret` between 1 and 100 by halving the range each time.
func findByHalving(secret: int): int {
    var low = 1;
    var high = 100;
    var tries = 0;
    while (true) {
        let guess = (low + high) // 2;
        tries = tries + 1;
        if (guess < secret) {
            println(i"{$ guess $}: too low");
            low = guess + 1;
        } else if (guess > secret) {
            println(i"{$ guess $}: too high");
            high = guess - 1;
        } else {
            let noun = if (tries == 1) { "guess" } else { "guesses" };
            println(i"{$ guess $}: found in {$ tries $} {$ noun $}");
            break;
        }
    }
    return tries;
}

if (__name__ == "__main__") {
    findByHalving(73);
    findByHalving(50);
}
```

*Listing 4-6: Binary search with while, break and an if expression*

```text
50: too low
75: too high
62: too low
68: too low
71: too low
73: found in 6 guesses
50: found in 1 guess
```

## Summary

- `if`, `while` and `for` headers always take parentheses (`E_CONDITION_PARENS`), and conditions must be `bool`.
- `if` is also an expression; there is no ternary operator.
- `for (x in items)` iterates lists, dict keys, `entries()` and generators; `start..end` is an exclusive range that only appears in loop headers and comprehensions.
- The loop variable is immutable; write `for (var x in …)` to get a changeable copy. A counted `for` needs a `var` counter.
- `break` and `continue` take an optional label to leave or continue an outer loop.
- `let (a, b) = …` and `let [a, b] = …` destructure tuples and lists.
- `match` on strings needs a `_` arm, or an unmatched value throws `MatchError`.
- For cleanup, use `try`/`finally`, `with` on a `Closeable` resource, or `del`. `defer` is accepted but does not run in Loup 1.25.

## Try It Yourself

1. Print FizzBuzz for 1 to 30 using `for (n in 1..31)` and a `match` on `(n % 3, n % 5)` — tuple patterns such as `(0, 0) => "FizzBuzz"` work in `match`.
2. Write a `Closeable` class `Timer` that prints how many loop iterations ran when it is closed, and use it in a `with` around a loop that `break`s early.
3. Rewrite Listing 4-3 without a label, using a `var found = false;` flag. Which version is easier to read?
