# Chapter 5: Control Flow

**What you'll learn.** `if` / `else if` / `else` as statements and expressions, `while`, `do`/`while`, the two `for` loops, labeled `break` and `continue`, `match`, the cleanup tools (`try`/`finally`, `defer`, `with`), and the propagation operators `?` and `attempt`.

**Why it matters.** Most of Loup's control flow looks like C, TypeScript or Python, but a handful of rules differ: every condition is parenthesized, there is no ternary operator, `elif` is not a statement keyword, and `match` is checked for exhaustiveness. Knowing these up front saves a round of compiler errors.

The listings in this chapter are `@mode(web.compiled)` programs. As Chapter 2 explains, `loup run` calls `main()` for you in that mode, so every program has one.

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

```loup
@mode(web.compiled)

func classify(celsius: int): string {
    if (celsius < 0) {
        return "freezing";
    } else if (celsius < 15) {
        return "cold";
    } else if (celsius < 25) {
        return "mild";
    } else {
        return "hot";
    }
}

func main(): void {
    for (t in [-5, 8, 20, 31]) {
        println(i"{$ t $}°C is {$ classify(t) $}");
    }
}
```

*Listing 5-1: An `else if` chain*

```text
-5°C is freezing
8°C is cold
20°C is mild
31°C is hot
```

Two rules are visible here. Every condition sits inside parentheses (ruling PAREN-1), and the chain is spelled `else if`. Python programmers tend to reach for `elif`, which Loup only knows inside template directives (Chapter 14):

```loup,does_not_compile
@mode(web.compiled)

func classify(celsius: int): string {
    if (celsius < 0) {
        return "freezing";
    } elif (celsius < 15) {
        return "cold";
    } else {
        return "warm";
    }
}

func main(): void {
    println(classify(4));
}
```

```text
main.lpweb:6:27: error[E0003]: expected `;`
main.lpweb:8:7: error[E0001]: expected expression
main.lpweb:8:12: error[E0003]: expected `;`
main.lpweb:10:5: error[E0001]: expected expression
main.lpweb:11:1: error[E0001]: expected expression
```

The parser reads `elif` as an ordinary name, so the messages are about a missing `;` rather than about `elif`. When an `if` chain produces a burst of "expected `;`" errors, look for an `elif`. Writing `} else if (celsius < 15) {` fixes it, as in Listing 5-1.

The parentheses are required even around a simple comparison:

```loup,does_not_compile
@mode(web.compiled)

func main(): void {
    let stock = 3;
    if stock < 5 {
        println("reorder soon");
    }
}
```

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

This message names the fix. The same rule applies to `while` conditions and `for` headers.

### Conditions must be `bool`

Loup has no truthiness: `0`, `""` and an empty list are not `false`. The Language Reference (§1-6.3) makes a non-`bool` condition the compile error `E_CONDITION_NOT_BOOL`. Loup 1.25.1 does not check this yet, so the mistake only surfaces when the program runs:

```loup,throws
@mode(web.compiled)

func main(): void {
    let pending = 3;
    if (pending) {
        println("work to do");
    }
}
```

```text
Uncaught RuntimeError: internal interpreter limitation: type error: CondBranch on non-bool: Int(3)
```

Write the comparison you mean, `if (pending > 0) { … }`.

## `if` as an expression

An `if` with an `else` produces a value. The value of each branch is its last expression, written without a semicolon:

```loup
@mode(web.compiled)

func main(): void {
    let items = 7;
    let label = if (items == 1) { "item" } else { "items" };
    println(i"{$ items $} {$ label $}");

    let delta = -4;
    let sign = if (delta > 0) { 1 } else if (delta < 0) { -1 } else { 0 };
    println(sign);
}
```

*Listing 5-2: `if` expressions*

```text
7 items
-1
```

There is no `?:` ternary operator (ruling SQ-3). A TypeScript habit gives an unhelpful message:

```loup,does_not_compile
@mode(web.compiled)

func main(): void {
    let items = 7;
    let label = items == 1 ? "item" : "items";
    println(label);
}
```

```text
main.lpweb:5:30: error[E0003]: expected `;`
```

A `?` after an operand is always the propagation operator (covered at the end of this chapter), so the parser stops at the string that follows it. Use the `if` expression from Listing 5-2.

The Language Reference requires the `else` branch on an `if` expression, because every path must produce a value. Loup 1.25.1 does not enforce this yet and gives the missing branch an empty value, so always write the `else`.

## Loops

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

```loup
@mode(web.compiled)

func main(): void {
    var n = 3;
    while (n > 0) {
        println(i"{$ n $}...");
        n = n - 1;
    }
    println("liftoff");

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

*Listing 5-3: `while` and `do`/`while`*

```text
3...
2...
1...
liftoff
attempt 1
attempt 2
```

A `do`/`while` runs its body at least once and ends with a semicolon after the condition.

### `for` over a collection or a range

The `for (x in source)` form visits each element of a list, a set, a dict's keys, or the range form `start..end`:

```loup
@mode(web.compiled)

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

    for (i in 0..3) {
        println(i"row {$ i $}");
    }

    let prices = {"apple": 120, "banana": 45};
    for ((name, cents) in prices.entries()) {
        println(i"{$ name $} costs {$ cents $} cents");
    }

    for (var i = 0; i < 10; i = i + 4) {
        println(i);
    }
}
```

*Listing 5-4: `for` over a list, a range and dict entries, and the counted `for`*

```text
apple
banana
cherry
row 0
row 1
row 2
apple costs 120 cents
banana costs 45 cents
0
4
8
```

The range `0..3` visits 0, 1 and 2: the end is exclusive and there is no inclusive form. A range is part of the loop header, not a value (ruling SQ-4), so it cannot be stored or passed:

```loup,does_not_compile
@mode(web.compiled)

func main(): void {
    let firstThree = 0..3;
    for (i in firstThree) {
        println(i);
    }
}
```

```text
main.lpweb:4:22: 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 `for (i in 0..3)` directly, or build a list if you need the numbers as data.

The loop variable is immutable. Assigning it is a mistake carried over from languages where the loop variable is an ordinary variable:

```loup,does_not_compile
@mode(web.compiled)

func main(): void {
    let prices = [100, 250, 80];
    for (p in prices) {
        p = p + p // 10;
        println(p);
    }
}
```

```text
main.lpweb:6:9: error[E0001]: cannot assign to immutable binding 'p'
```

Declare the loop variable with `var` when the body needs to change its copy. The change never reaches the list:

```loup
@mode(web.compiled)

func main(): void {
    let prices = [100, 250, 80];
    for (var p in prices) {
        p = p + p // 10;
        println(p);
    }
    println(prices);
}
```

*Listing 5-5: A mutable loop variable is a fresh copy each iteration*

```text
110
275
88
[100, 250, 80]
```

(`//` is integer floor division in Loup; comments start with `#`.)

Do not add to or remove from a collection while a `for` loop walks it. The Language Reference makes that the compile error `E_BORROW_LOOP_MUTATION`; Loup 1.25.1 does not check it, and on the interpreter a `push` inside such a loop can keep the loop running forever. Collect the changes in a second list and apply them after the loop.

### Labeled `break` and `continue`

A label names a loop, so `break` or `continue` can target an outer loop from inside an inner one:

```loup
@mode(web.compiled)

func main(): void {
    let shelves = [[4, 0, 7], [3, 9], [0, 0, 2]];
    var checked = 0;
    shelves: for (shelf in shelves) {
        for (count in shelf) {
            checked = checked + 1;
            if (count == 0) {
                continue shelves;
            }
            if (count > 8) {
                println(i"overstocked shelf found after {$ checked $} bins");
                break shelves;
            }
        }
    }
}
```

*Listing 5-6: Labeled loops*

```text
overstocked shelf found after 4 bins
```

The label `shelves` lives in its own namespace, so it does not clash with the variable `shelves`. Misspelling a label is a compile error:

```loup,does_not_compile
@mode(web.compiled)

func main(): void {
    rows: for (row in [[1, 2], [3, 4]]) {
        for (cell in row) {
            if (cell == 2) {
                continue row;
            }
        }
    }
}
```

```text
main.lpweb:7:17: error[E0001]: continue label 'row' not found
```

## `match`

`match` compares a value against patterns in order and evaluates the first arm that fits. It is an expression, and its arms are separated by commas:

```loup
@mode(web.compiled)

func describe(n: int): string {
    return match n {
        0 => "none",
        1 => "one",
        k if (k < 0) => "invalid",
        k if (k < 10) => "a few",
        _ => "many",
    };
}

func main(): void {
    for (n in [0, 1, -4, 7, 99]) {
        println(i"{$ n $}: {$ describe(n) $}");
    }

    let status = 404;
    match status {
        200 => println("ok"),
        404 => {
            println("not found");
            println("check the URL");
        },
        _ => println("unexpected status"),
    }
}
```

*Listing 5-7: Literal, binding, guard and wildcard patterns*

```text
0: none
1: one
-4: invalid
7: a few
99: many
not found
check the URL
```

The patterns used so far:

- a **literal** such as `0` or `404`;
- a **binding** such as `k`, which matches anything and names it;
- a **guard**, `k if (k < 0)`, which adds a condition in parentheses;
- the **wildcard** `_`, which matches anything without naming it;
- an arm body can be a single expression or a `{ … }` block.

The scrutinee is written without parentheses: `match n { … }`.

### Exhaustiveness

When you match on an enum, the compiler checks that every variant is handled. Chapter 8 covers enums in depth; this is the check you will meet most often:

```loup,does_not_compile
@mode(web.compiled)

enum Light { Red, Yellow, Green }

func next(light: Light): Light {
    return match light {
        Light.Red => Light.Green,
        Light.Green => Light.Yellow,
    };
}

func main(): void {
    println(next(Light.Red));
}
```

```text
main.lpweb:6:12: error[E0001]: Non-exhaustive match: missing variants Yellow
```

The message lists every missing variant. Add the arm:

```loup
@mode(web.compiled)

enum Light { Red, Yellow, Green }

func next(light: Light): Light {
    return match light {
        Light.Red => Light.Green,
        Light.Green => Light.Yellow,
        Light.Yellow => Light.Red,
    };
}

func main(): void {
    println(next(Light.Red));
    println(next(Light.Yellow));
}
```

*Listing 5-8: An exhaustive match over an enum*

```text
Green
Red
```

The Language Reference requires every `match` to be exhaustive. For an enum, Loup 1.25.1 enforces that at compile time; for an open type such as `int` or `string` it does not, and a value that no arm accepts raises `MatchError` when the program runs:

```loup,throws
@mode(web.compiled)

func describe(n: int): string {
    return match n {
        0 => "none",
        1 => "one",
    };
}

func main(): void {
    println(describe(5));
}
```

```text
Uncaught MatchError: No matching arm for value in match expression
  at describe (main.lpweb:4:12)
  at main (main.lpweb:11:13)
```

End a `match` over an open type with a `_` arm, as Listing 5-7 does.

### Tuple and type patterns

A tuple pattern matches element by element. Literals and bindings mix freely:

```loup
@mode(web.compiled)

func locate(point: (int, int)): string {
    return match point {
        (0, 0) => "origin",
        (x, 0) => i"on the x axis at {$ x $}",
        (0, y) => i"on the y axis at {$ y $}",
        (a, b) if (a == b) => "on the diagonal",
        _ => "somewhere else",
    };
}

func main(): void {
    for (p in [(0, 0), (3, 0), (0, -2), (4, 4), (1, 5)]) {
        println(locate(p));
    }
}
```

*Listing 5-9: Tuple patterns*

```text
origin
on the x axis at 3
on the y axis at -2
on the diagonal
somewhere else
```

> ⚠ **Known issue in 1.25.1.** If a later tuple-pattern arm binds the same name as an earlier one, for example `(x, 0) => …` followed by `(x, y) if (x == y) => …`, the earlier arm sees `None` for that name on the interpreter. Listing 5-9 uses different names (`a`, `b`) in the guard arm to avoid it.

A value whose type is a union, such as `int | string`, is matched with a type pattern, `int(n)`, that both checks the type and binds the value:

```loup
@mode(web.compiled)

func show(value: int | string): string {
    return match value {
        int(n) => i"number {$ n $}",
        string(s) => i"text '{$ s $}'",
    };
}

func main(): void {
    println(show(42));
    println(show("forty-two"));
}
```

*Listing 5-10: Matching a union type*

```text
number 42
text 'forty-two'
```

### No OR-patterns

Loup's pattern grammar has no `|` alternative (Language Reference §1-19.6), so a Rust-style `'a' | 'e' =>` arm does not parse:

```loup,does_not_compile
@mode(web.compiled)

func kind(letter: string): string {
    return match letter {
        "a" | "e" | "i" | "o" | "u" => "vowel",
        _ => "consonant",
    };
}

func main(): void {
    println(kind("e"));
}
```

```text
main.lpweb:5:13: error[E0001]: expected FatArrow, found Pipe
main.lpweb:5:13: error[E0001]: expected expression
main.lpweb:5:19: error[E0001]: expected FatArrow, found Pipe
main.lpweb:5:19: error[E0001]: expected expression
main.lpweb:5:25: error[E0001]: expected FatArrow, found Pipe
main.lpweb:5:25: error[E0001]: expected expression
main.lpweb:5:31: error[E0001]: expected FatArrow, found Pipe
main.lpweb:5:31: error[E0001]: expected expression
```

Use a guard instead:

```loup
@mode(web.compiled)

func kind(letter: string): string {
    return match letter {
        l if ("aeiou".contains(l)) => "vowel",
        _ => "consonant",
    };
}

func main(): void {
    println(kind("e"));
    println(kind("k"));
}
```

*Listing 5-11: A guard in place of an OR-pattern*

```text
vowel
consonant
```

### Reserved words are not names

Some ordinary-looking words are reserved (Language Reference §1-3.4). `where`, for example, introduces generic constraints, so it cannot name a variable:

```loup,does_not_compile
@mode(web.compiled)

func main(): void {
    let where = "warehouse 3";
    println(where);
}
```

```text
main.lpweb:4:9: error[E0001]: `where` is a reserved word (§1-3.4) and cannot name a binding
  note: rename the binding — e.g. `where_value`
```

Pick another name, such as `location`.

## Cleaning up: `try`/`finally`, `defer` and `with`

### `try`, `catch` and `finally`

Loup has exceptions. A `catch` clause names the exception type, then `as`, then the binding. `finally` runs whether the `try` body finished, threw, or was handled:

```loup
@mode(web.compiled)

func average(total: int, count: int): int {
    return total // count;
}

func main(): void {
    for (count in [4, 0]) {
        try {
            println(i"average: {$ average(100, count) $}");
        } catch (IntegerDivisionError | ValueError as e) {
            println(i"skipped: {$ e.message $}");
        } finally {
            println(i"done with count {$ count $}");
        }
    }
}
```

*Listing 5-12: `try` / `catch` / `finally`*

```text
average: 25
done with count 4
skipped: integer division or modulo by zero
done with count 0
```

`catch (A | B as e)` catches either type. Clauses are tried in order, so put specific types before general ones such as `Exception`. Chapter 12 covers the exception hierarchy.

The catch syntax is `Type as name`, not the `name: Type` form of TypeScript or Kotlin:

```loup,does_not_compile
@mode(web.compiled)

func main(): void {
    try {
        throw ValueError("bad quantity");
    } catch (e: ValueError) {
        println(e.message);
    }
}
```

```text
main.lpweb:6:15: error[E0001]: expected RParen, found Colon
main.lpweb:6:15: error[E0002]: expected `{`
main.lpweb:6:15: error[E0001]: expected expression
main.lpweb:6:17: error[E0003]: expected `;`
main.lpweb:9:1: error[E0001]: expected expression
```

The first line is the one that matters: the parser wanted `)` where it found `:`. Write `catch (ValueError as e)`.

A `finally` block cannot change where control goes. `return`, `break` or `continue` inside it is `E_FINALLY_CONTROL`.

### `defer`

`defer` registers a statement or block to run when the enclosing block ends, however it ends. Several `defer`s run in last-in, first-out order (Language Reference §1-6.5).

> ⚠ **Known gap in 1.25.1.** `defer` compiles, but the deferred code never runs, on the interpreter or in a Wasm build. Until that is fixed, use `try { … } finally { … }` for cleanup that must happen.

This listing shows the gap. By the specification it would print `cleanup 2` and `cleanup 1` after `working`:

```loup,not_desired_behavior
@mode(web.compiled)

func work(): int {
    defer { println("cleanup 1"); }
    defer { println("cleanup 2"); }
    println("working");
    return 42;
}

func main(): void {
    println(work());
}
```

```text
working
42
```

The same cleanup written with `finally` runs today:

```loup
@mode(web.compiled)

func work(): int {
    try {
        println("working");
        return 42;
    } finally {
        println("cleanup");
    }
}

func main(): void {
    println(work());
}
```

*Listing 5-13: `finally` as the working substitute for `defer`*

```text
working
cleanup
42
```

### `with` and `Scoped[T]`

`with (name = expr) { … }` brackets a block with setup and teardown. The value of `expr` must implement `Scoped[T]`: the runtime calls `scope_enter()` and binds its result to `name`, runs the block, then calls `scope_exit(error)`, passing `None` or the exception that is leaving the block (Language Reference §1-6.6). Returning `false` from `scope_exit` lets an exception continue on its way.

```loup
@mode(web.compiled)

class Timer implements Scoped[string] {
    pub let label: string;
    new(label: string) { this.label = label; }

    pub func scope_enter(): string {
        println(i"start {$ this.label $}");
        return this.label;
    }

    pub func scope_exit(error: Option[Any]): bool {
        println(i"stop {$ this.label $}");
        return false;
    }
}

func main(): void {
    with (name = Timer("render")) {
        println(i"working in {$ name $}");
    }

    try {
        with (name = Timer("save")) {
            throw ValueError("disk full");
        }
    } catch (ValueError as e) {
        println(i"caught: {$ e.message $}");
    }
}
```

*Listing 5-14: A `Scoped` resource*

```text
start render
working in render
stop render
start save
stop save
caught: disk full
main.lpweb:12:39: warning: W_ANY_IN_AOT_MODE: use of `Any` in web.compiled mode prevents AOT specialisation — consider a static type if performance is critical
main.lpweb:12:39: warning: W_ANY_IN_AOT_MODE: use of `Any` in web.compiled mode prevents AOT specialisation — consider a static type if performance is critical
```

The `stop save` line appears before the exception reaches the `catch`: teardown runs even when the block throws. The warnings come from the `Option[Any]` parameter that the `Scoped` protocol specifies. `W_ANY_IN_AOT_MODE` is an advisory, not an error (Chapter 2); the Language Reference exempts this one parameter from it, but 1.25.1 still prints it.

A `with` on a value that is not `Scoped` is rejected:

```loup,does_not_compile
@mode(web.compiled)

class Timer {
    pub let label: string;
    new(label: string) { this.label = label; }
}

func main(): void {
    with (t = Timer("render")) {
        println("working");
    }
}
```

```text
main.lpweb:9:5: error[E0001]: E_WITH_NOT_SCOPED: type 'Timer' does not implement Scoped[T] — a `with` resource must provide scope_enter() and scope_exit(error) (Closeable and Releasable do)
main.lpweb:9:5: error[E0001]: E_WITH_NOT_SCOPED: type 'Timer' does not implement Scoped[T] — a `with` resource must provide scope_enter() and scope_exit(error) (Closeable and Releasable do)
main.lpweb:9:5: error[E0001]: E_WITH_NOT_SCOPED: type 'Timer' does not implement Scoped[T] — a `with` resource must provide scope_enter() and scope_exit(error) (Closeable and Releasable do)
```

(1.25.1 prints the same diagnostic three times.) Add `implements Scoped[…]` and the two methods, as in Listing 5-14.

## Propagating errors: `?` and `attempt`

Functions that can fail without it being exceptional return `Result[T, E]` (Chapter 8 and Chapter 12). The postfix `?` operator unwraps an `Ok` or returns the `Err` from the enclosing function:

```loup
@mode(web.compiled)

func parsePort(text: string): Result[int, string] {
    if (text == "") {
        return Err("empty port");
    }
    return Ok(text.to_int());
}

func address(host: string, port: string): Result[string, string] {
    let p = parsePort(port)?;
    return Ok(i"{$ host $}:{$ p $}");
}

func main(): void {
    for (port in ["8080", ""]) {
        match address("localhost", port) {
            Ok(a) => println(a),
            Err(e) => println(i"error: {$ e $}"),
        }
    }
}
```

*Listing 5-15: `?` on a `Result`*

```text
localhost:8080
error: empty port
```

`?` works only inside a function that returns `Result`. Using it on an `Option`, in a function that returns `Option`, is a compile error:

```loup,does_not_compile
@mode(web.compiled)

func firstWord(line: string): Option[string] {
    let parts = line.split(" ");
    if (parts[0] == "") {
        return None;
    }
    return Some(parts[0]);
}

func shout(line: string): Option[string] {
    let word = firstWord(line)?;
    return Some(word.to_uppercase());
}

func main(): void {
    println(repr(shout("hello world")));
}
```

```text
main.lpweb:12:16: error[E_PROPAGATE_WRONG_RETURN]: E_PROPAGATE_WRONG_RETURN: the '?' operator can only be used inside a function returning Result (§1-5.7)
```

For an `Option`, match on it, or supply a default with `??`:

```loup
@mode(web.compiled)

func firstWord(line: string): Option[string] {
    let parts = line.split(" ");
    if (parts[0] == "") {
        return None;
    }
    return Some(parts[0]);
}

func shout(line: string): Option[string] {
    return match firstWord(line) {
        Some(word) => Some(word.to_uppercase()),
        None => None,
    };
}

func main(): void {
    println(repr(shout("hello world")));
    println(repr(shout("")));
    println(firstWord("") ?? "(no words)");
}
```

*Listing 5-16: Handling an `Option` with `match` and `??`*

```text
Some("HELLO")
None
(no words)
```

`repr` prints a value in its diagnostic form, which shows `Some(…)` and `None`; `println` on an `Option` prints the inner value, or nothing for `None`.

`attempt` is the in-place counterpart of `?` (Language Reference §1-5.10). Inside a `try` block, `attempt expr` unwraps an `Ok`, or jumps to the block's `catch Err(e)` arm with the error value, without leaving the function:

```loup
@mode(web.compiled)

func parseQty(text: string): Result[int, string] {
    if (text == "") {
        return Err("quantity is empty");
    }
    return Ok(text.to_int());
}

func main(): void {
    for (raw in ["3", ""]) {
        try {
            let qty = attempt parseQty(raw);
            println(i"qty = {$ qty $}");
        } catch Err(e) {
            println(i"bad input: {$ e $}");
        } finally {
            println("checked");
        }
    }
}
```

*Listing 5-17: `attempt` with a `catch Err` arm*

```text
qty = 3
checked
bad input: quantity is empty
checked
```

A `try` block that uses `attempt` must have a `catch Err` arm; it can also have ordinary `catch (T as e)` clauses for exceptions.

## A worked example: checking an order

This program combines a `for` loop with tuple destructuring, `?` propagation, and a `match` with block arms:

```loup
@mode(web.compiled)

func parseQuantity(text: string): Result[int, string] {
    if (text == "") {
        return Err("missing quantity");
    }
    let n = text.to_int();
    if (n <= 0) {
        return Err(i"'{$ text $}' is not a positive quantity");
    }
    return Ok(n);
}

func checkLine(item: string, qtyText: string, stock: int): Result[int, string] {
    let qty = parseQuantity(qtyText)?;
    if (qty > stock) {
        return Err(i"only {$ stock $} left");
    }
    return Ok(qty);
}

func main(): void {
    let lines = [("pens", "3"), ("paper", ""), ("ink", "12"), ("tape", "x")];
    var units = 0;
    var rejected = 0;
    for ((item, qty) in lines) {
        match checkLine(item, qty, 10) {
            Ok(n) => {
                units = units + n;
                println(i"ok      {$ item $} x{$ n $}");
            },
            Err(reason) => {
                rejected = rejected + 1;
                println(i"reject  {$ item $}: {$ reason $}");
            },
        }
    }
    println(i"{$ units $} units accepted, {$ rejected $} lines rejected");
}
```

*Listing 5-18: Validating order lines*

```text
ok      pens x3
reject  paper: missing quantity
reject  ink: only 10 left
reject  tape: 'x' is not a positive quantity
3 units accepted, 3 lines rejected
```

`"x".to_int()` returns `0` in 1.25.1 rather than reporting an error, which is why `parseQuantity` checks for a positive result.

## Summary

- Conditions go in parentheses and must be `bool`; chains use `else if`. 1.25.1 catches missing parentheses at compile time but a non-`bool` condition only at run time.
- `if (c) { a } else { b }` is an expression. There is no ternary operator.
- `while`, `do { … } while (c);`, `for (x in xs)`, `for (i in a..b)` (exclusive end, loop headers only) and the counted `for (var i = 0; …; …)`. The loop variable is immutable unless declared `var`.
- Labels let `break` and `continue` target an outer loop.
- `match` arms are `pattern => expr`, separated by commas, with literals, bindings, guards, `_`, tuple patterns and type patterns. There are no OR-patterns. Enum matches are checked for exhaustiveness at compile time; other matches fall through to `MatchError` at run time without a `_` arm.
- `try` / `catch (T as e)` / `finally` handle exceptions. `defer` is specified but does not run in 1.25.1, so use `finally`. `with` drives any `Scoped[T]` value.
- `?` propagates the `Err` of a `Result` from a function that returns `Result`; `attempt` sends it to the enclosing `try`'s `catch Err` arm. Handle an `Option` with `match` or `??`.
