The Loup Native Book — contents
  1. Chapter 1: Getting started
  2. Chapter 2: Native mode overview
  3. Chapter 3: Variables and numerics
  4. Chapter 4: Functions
  5. Chapter 5: Control flow
  6. Chapter 6: Collections
  7. Chapter 7: Structs
  8. Chapter 8: Enums and pattern matching
  9. Chapter 9: Classes and ARC
  10. Chapter 10: Interfaces
  11. Chapter 11: Generics
  12. Chapter 12: Error handling
  13. Chapter 13: Modules and packages
  14. Chapter 14: Template fences
  15. Chapter 15: Ownership verbs
  16. Chapter 16: borrow, excl borrow and noescape
  17. Chapter 17: unique ownership
  18. Chapter 18: Lifetimes and regions
  19. Chapter 19: @unsafe and calling C
  20. Chapter 20: Concurrency with OS threads
  21. Chapter 21: Async on native
  22. Chapter 22: Desktop UI
  23. Chapter 23: SIMD and GPU
  24. Chapter 24: Building and distributing
  25. Chapter 25: Memory safety patterns

Chapter 5: Control flow

Loup's control flow is close to C's and TypeScript's, with a few rules that catch people out: conditions always take parentheses, bodies always take braces, there is no ?: operator, and loop variables are immutable unless you ask otherwise. This chapter covers conditionals, the loop forms, match, scoped resources with with, and exceptions, and points out where Loup 1.25.1 does not yet do what the specification says.

Conditionals

@mode(host.native)

func describe(n: int): string {
    if (n < 0) {
        return "negative";
    } else if (n == 0) {
        return "zero";
    } else {
        return "positive";
    }
}

func main(): void {
    println(describe(-3));
    println(describe(0));
}

Listing 5-1: if, else if, else

negative
zero

The parentheses around a condition are part of the syntax (ruling PAREN-1), and so are the braces around each branch, even for a single statement. Leaving out the parentheses, as you would in Rust, Go or Swift, gives a message that says exactly what to do:

This code does not compile
@mode(host.native)

func main(): void {
    let load = 7;
    if load > 3 {
        println("busy");
    }
}
main.lphc:5:8: error[E0001]: E_CONDITION_PARENS: the condition of `if` must be in parentheses; add them: `if (...) {`

while conditions and for headers need parentheses for the same reason. A branch without braces, if (load > 3) println("busy");, is error[E0002]: expected {``.

if is an expression

An if with an else produces a value, so it can sit on the right of a let. Loup has no ternary operator; this is the replacement:

This code does not compile
@mode(host.native)

func main(): void {
    let load = 7;
    let state = load > 3 ? "busy" : "idle";
    println(state);
}
main.lphc:5:28: error[E0003]: expected `;`

The message is unhelpful because the parser does not know ? in this position. Write an if expression:

let load = 7;
let state = if (load > 3) { "busy" } else { "idle" };
println(state);

Listing 5-2: An if expression instead of ?:

busy

Each branch is a block whose last expression is its value; both branches must produce the same type. This is also how you initialize a let whose value depends on a condition.

Loops

Loup has four loop forms: while, do … while, the counted C-style for, and for … in over anything iterable.

func main(): void {
    var attempts = 0;
    while (attempts < 3) {
        attempts = attempts + 1;
    }

    var polls = 0;
    do {
        polls = polls + 1;
    } while (polls < 2);

    for (var i = 0; i < 3; i = i + 1) {
        print(i);
    }
    println("");

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

    for (host in ["alpha", "beta"]) {
        println(i"ping {$ host $}");
    }
    println(i"{$ attempts $} attempts, {$ polls $} polls");
}

Listing 5-3: The four loop forms

012
012
ping alpha
ping beta
3 attempts, 2 polls

The range start..end excludes end, and it is not a value: it is allowed only as the source of a for loop or a comprehension. let r = 0..3; is E_RANGE_FORM_MISPLACED. There is no inclusive range; write 0..n + 1.

break leaves the innermost loop and continue starts its next iteration. To leave an outer loop, label it:

let grid = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
var found = -1;
outer: for (row in grid) {
    for (cell in row) {
        if (cell * cell > 30) {
            found = cell;
            break outer;
        }
    }
}
println(found);

Listing 5-4: Breaking out of nested loops with a label

6

Loop variables are immutable

The variable a for … in loop binds is a let. Assigning to it is an error:

This code does not compile
@mode(host.native)

func main(): void {
    let prices = [100, 250];
    for (p in prices) {
        p = p + p // 10;
        println(p);
    }
}
main.lphc:6:9: error[E0001]: cannot assign to immutable binding 'p'

Declare it var if you want a scratch copy you can change. Each iteration gets a fresh copy, so assigning to it never changes the list:

let prices = [100, 250];
for (var p in prices) {
    p = p + p // 10;
    println(p);
}
println(prices);

Listing 5-5: A var loop variable

110
275
[100, 250]

To build a changed list, use a comprehension: [p + p // 10 for p in prices] (chapter 6).

Changing a collection while iterating over it

In C++, appending to a std::vector while iterating over it can invalidate the iterator. The Loup specification makes the equivalent a compile-time error: a for (x in items) loop borrows items for the whole body, and mutating items inside is E_BORROW_LOOP_MUTATION (Language Reference §1-6.4). Stage0 1.25.1 does not check this yet, and the loop sees elements appended while it runs:

This code does not do what it should
@mode(host.native)

func main(): void {
    var orderLines = [1200, 800, 1500];
    var visited = 0;
    for (amount in orderLines) {
        visited = visited + 1;
        if (amount > 1000) {
            orderLines.push(amount // 100);   # an insurance line for large orders
        }
    }
    println(i"visited {$ visited $} lines: {$ orderLines $}");
}
visited 5 lines: [1200, 800, 1500, 12, 15]

The loop was meant to visit the three original lines; it visited the two new ones too. If a new line had itself been over 1000, the loop would never end. Collect the additions separately and append them after the loop, which is also what the specified rule will require:

var orderLines = [1200, 800, 1500];
var insurance: list[int] = [];
for (amount in orderLines) {
    if (amount > 1000) {
        insurance.push(amount // 100);
    }
}
orderLines = orderLines + insurance;
println(orderLines);

Listing 5-6: Collecting changes, then applying them

[1200, 800, 1500, 12, 15]

match

match compares a value against patterns and evaluates the first arm that matches. There are no parentheses around the value being matched, and arms are separated by commas:

func classify(status: int): string {
    return match status {
        200 => "ok",
        404 => "not found",
        code if (code >= 500) => i"server error {$ code $}",
        _ => "other",
    };
}

func main(): void {
    println(classify(200));
    println(classify(503));
    println(classify(302));
}

Listing 5-7: Literal patterns, a guard, and a wildcard

ok
server error 503
other

A plain name such as code matches anything and binds it; if (…) after it is a guard; _ matches anything without binding. Alternatives such as 301 | 302 => are not supported in 1.25.1; use a guard (code if (code == 301 or code == 302)).

A match must cover every possible value. That matters most for enums, where adding a variant makes every match that forgot it fail to compile:

This code does not compile
@mode(host.native)

enum Light { Red, Amber, Green }

func action(light: Light): string {
    return match light {
        Light.Red => "stop",
        Light.Green => "go",
    };
}

func main(): void {
    println(action(Light.Amber));
}
main.lphc:6:12: error[E0001]: Non-exhaustive match: missing variants Amber

Add the missing arm (Light.Amber => "slow") or a _ arm. Chapter 8 covers enums, destructuring patterns, and Option and Result.

Scoped resources with with

with (name = expr) { … } runs a block with a resource and guarantees that the resource is told when the block ends, normally or by an exception. The resource implements the Scoped[T] interface: scope_enter() returns the value bound to name, and scope_exit(error) receives None or Some(exception) (Language Reference §1-6.6). Returning false from scope_exit lets an exception continue on its way.

@mode(host.native)

class Transaction implements Scoped[Transaction] {
    pub let name: string;

    new(name: string) { this.name = name; }

    pub func scope_enter(): Transaction {
        println(i"begin {$ this.name $}");
        return this;
    }

    pub func scope_exit(error: Option[Exception]): bool {
        match error {
            None => println(i"commit {$ this.name $}"),
            Some(e) => println(i"rollback {$ this.name $}: {$ e.message $}"),
        }
        return false;
    }
}

func transfer(amount: int): void {
    with (tx = Transaction(i"transfer {$ amount $}")) {
        if (amount > 1000) {
            throw ValueError("amount over limit");
        }
        println(i"moved {$ amount $}");
    }
}

func main(): void {
    transfer(50);
    try {
        transfer(5000);
    } catch (ValueError as e) {
        println(i"caught: {$ e.message $}");
    }
}

Listing 5-8: A transaction that commits or rolls back

begin transfer 50
moved 50
commit transfer 50
begin transfer 5000
rollback transfer 5000: amount over limit
caught: amount over limit

This is the Loup equivalent of Python's context managers and C#'s using. Native programs use it for files, locks and connections whose release must not wait for a reference count to reach zero.

⚠ Planned. defer stmt; and defer { … }, which run cleanup when the enclosing block exits (Language Reference §1-6.5), parse in Loup 1.25.1 but do not work yet: the interpreter skips the deferred code entirely, and a native build refuses the program (E-BUILD-NATIVE: … an unsupported operation). Use with or try/finally for cleanup until defer lands.

Exceptions

throw raises an exception; try/catch/finally handles it. The catch clause names the exception type, then as, then the binding:

@mode(host.native)

class ConfigError : Exception {
    new(msg: string) { super(msg); }
}

func loadPort(text: string): int {
    try {
        let n = text.toInt();
        if (n <= 0) {
            throw ConfigError(i"port must be positive, got {$ n $}");
        }
        return n;
    } catch (ConfigError as e) {
        println(i"config error: {$ e.message $}");
        return 8080;
    } finally {
        println("checked port");
    }
}

func main(): void {
    println(loadPort("9000"));
    println(loadPort("-1"));
}

Listing 5-9: try, catch, finally

checked port
9000
config error: port must be positive, got -1
checked port
8080

finally runs on every path out of the try, including the return inside it, which is why "checked port" prints before the caller prints the value. Chapter 12 covers the exception hierarchy, catch (A | B as e), chaining with throw … from e, Result values and the ? operator, and what an uncaught exception looks like in a native binary.

Summary

  • Conditions take parentheses (E_CONDITION_PARENS) and bodies take braces. if with else is an expression and replaces the ternary operator, which does not exist.
  • Loops: while, do … while, for (var i = 0; …; …), and for (x in xs). start..end excludes end and is only valid as a loop source.
  • A for … in variable is immutable; write for (var x in xs) for a mutable copy.
  • Mutating a list inside a loop over it should be E_BORROW_LOOP_MUTATION; 1.25.1 does not check it and the loop sees the new elements, so collect changes and apply them afterwards.
  • match is exhaustive, supports literals, bindings, guards and _; | alternatives are not supported yet.
  • with (x = resource) { … } calls scope_enter and scope_exit on every exit path. defer is ⚠ planned: it parses but does not run.
  • catch (Type as e) catches by type; finally always runs. Chapter 6 covers collections.