# Chapter 4: Functions and Control Flow

Most of Loup's everyday syntax is the same in `@mode(freestanding)` as in any other mode: `func`, `if`/`else`, `while`, `for`, `match`, `return`, `break`, `continue` and lambdas. This chapter is a quick tour of that syntax, with the mistakes C and Rust programmers make most often, and then the parts that need care in freestanding: closures, generators and `async`.

## Function declarations

```loup
@mode(freestanding)

# Parameters and the return type are always written out.
func add(a: uint32, b: uint32): uint32 {
    return a + b
}

func clamp(x: int32, lo: int32, hi: int32): int32 {
    if (x < lo) { return lo }
    if (x > hi) { return hi }
    return x
}

# A default value is evaluated on every call that omits the argument.
func scale(x: uint32, factor: uint32 = 2u32): uint32 {
    return x * factor
}

func nop(): void { }

pub func demo(): uint32 {
    return add(1u32, 2u32) + scale(3u32) + scale(3u32, factor = 4u32)
}
```

*Listing 4-1: Function declarations, a default parameter and a named argument*

A few rules carry over from the rest of the language:

- The keyword is `func`, and the return type follows a colon. There is no `fn name(...) -> T` form; `fn` appears only in function *types* such as `fn(int): void`.
- `pub func` exports a function from its module. Without `pub` it is private to the module.
- Arguments with defaults can be passed by name: `scale(3u32, factor = 4u32)`.
- `#` starts a comment. `//` is the floor-division operator, so a C-style `// comment` is a syntax error.

The default calling convention in freestanding is the platform C ABI. `@callconv(...)` overrides it for functions that are called from firmware or assembly — for example `win64` for a UEFI entry point (chapter 16):

```loup
#~ @mode(freestanding)
#~
@callconv(win64)
func efiMain(imageHandle: uint64, systemTable: uint64): uint64 {
    return 0u64      # EFI_SUCCESS
}
```

*Listing 4-2: Overriding the calling convention*

## Conditions need parentheses

Every condition — `if`, `while`, and the header of a `for` — is written in parentheses, and every body in braces. A Rust or Go habit trips this rule:

```loup,does_not_compile
@mode(freestanding)

func classify(irq: uint32): uint8 {
    if irq < 32u32 {
        return 0u8          # CPU exception
    }
    return 1u8              # device interrupt
}
```

```text
error[E0001]: E_CONDITION_PARENS: the condition of `if` must be in parentheses; add them: `if (...) {`
 --> src/main.lpfree:4:8
  |
4 |     if irq < 32u32 {
  |        ^^^^^^^^^^^
  |
```

The message says exactly what to write: `if (irq < 32u32) {`.

## `if`, `while`, `for`

```loup
#~ @mode(freestanding)
#~
func irqClass(irq: uint32): uint8 {
    if (irq < 32u32) {
        return 0u8                    # CPU exception
    } else if (irq < 48u32) {
        return 1u8                    # legacy PIC line
    } else {
        return 2u8                    # MSI / APIC vector
    }
}

# Busy-wait until bit 5 of a status value is set, giving up after `limit` polls.
func pollReady(readStatus: fn(): uint8, limit: uint32): bool {
    var tries: uint32 = 0
    while (tries < limit) {
        if ((readStatus() & 0x20u8) != 0u8) {
            return true
        }
        tries = tries + 1u32
    }
    return false
}

func sumFirst(n: int): uint64 {
    var total: uint64 = 0
    for (var i = 0; i < n; i = i + 1) {        # C-style three-part header
        total = total + (i as uint64)
    }
    for (j in 0..4) {                          # a range as the loop source
        total = total + (j as uint64)
    }
    return total
}

func firstDivisibleBy7(n: int): int {
    var i = 0
    while (i < n) {
        if (i > 0 and i % 7 == 0) {
            return i
        }
        i = i + 1
    }
    return 0 - 1                               # not found
}
```

*Listing 4-3: Conditionals and loops*

There is no `loop { … }` keyword; write `while (true) { … }`, which is also how a kernel's idle loop and a `noreturn` function end (chapter 17). `break` and `continue` work in every loop. A range such as `0..4` is allowed only as a loop source.

`if` is also an expression, and it is the only conditional expression: Loup has no `?:` ternary.

```loup,does_not_compile
@mode(freestanding)

func pageKind(size: uint64): uint8 {
    let kind = size > 4096u64 ? 2u8 : 1u8
    return kind
}
```

```text
error[E0003]: expected `;`
 --> src/main.lpfree:4:33
  |
4 |     let kind = size > 4096u64 ? 2u8 : 1u8
  |                                 ^^^
  |
```

The parser reads `size > 4096u64` as a complete expression, sees `?` (the error-propagation operator) and then cannot continue. Write the `if` form:

```loup
#~ @mode(freestanding)
#~
func pageKind(size: uint64): uint8 {
    let kind = if (size > 4096u64) { 2u8 } else { 1u8 }
    return kind
}
```

*Listing 4-4: `if` as an expression instead of a ternary*

### Iterating a buffer

`for (x in …)` works over ranges and strings. A fixed-size `buffer[T, N]` (chapter 13) is a plain block of memory with no iterator, so a `for`-`in` loop over one is rejected:

```loup,does_not_compile
@mode(freestanding)

func checksum(): uint64 {
    var block: buffer[uint64, 8];
    var sum: uint64 = 0
    for (word in block) {
        sum = sum ^ word
    }
    return sum
}
```

```text
error[E0001]: type 'buffer[uint64, 8]' has no method 'iter'
 --> src/main.lpfree:6:5
  |
6 |     for (word in block) {
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
```

Index it instead. The element count is part of the type, so the bound is known:

```loup
#~ @mode(freestanding)
#~
func checksum(): uint64 {
    var block: buffer[uint64, 8];
    var sum: uint64 = 0
    for (var i = 0; i < 8; i = i + 1) {
        sum = sum ^ block[i]
    }
    return sum
}

func serialChecksum(message: string): uint8 {
    var sum: uint8 = 0
    for (c in message) {                 # strings do iterate
        sum = sum ^ (c as uint8)
    }
    return sum
}
```

*Listing 4-5: Indexing a buffer; iterating a string*

## `match`

`match` takes the scrutinee without parentheses, and every arm is `pattern => expression` or `pattern => { block }`. It must be exhaustive.

```loup
#~ @mode(freestanding)
#~
enum Irq {
    Timer,
    Keyboard,
    Serial(port: uint16),
    Unknown(vector: uint32),
}

func irqName(irq: Irq): string {
    return match irq {
        Irq.Timer => "timer",
        Irq.Keyboard => "keyboard",
        Irq.Serial(port) => "serial",
        Irq.Unknown(vector) => "unknown",
    }
}

func isLegacy(irq: Irq): bool {
    match irq {
        Irq.Unknown(vector) if (vector >= 48u32) => { return false }
        _ => { return true }
    }
}
```

*Listing 4-6: `match` as an expression and as a statement, with a guard*

Chapter 6 covers patterns, guards and the exhaustiveness rule in detail. Exhaustiveness matters more in freestanding than elsewhere: there is no exception to catch an unexpected value at run time.

## `defer`

The Language Reference defines `defer { … }` to run a block when the enclosing scope exits, on every path — a normal return, an early `return`, or an early exit through `?` (§1-6.11). That would make it the natural place to release a lock. In Loup 1.25 it does not work that way, and the failure is silent: the checker accepts the statement, but the deferred block is never executed — the interpreters skip it, and the freestanding back ends treat the defer bookkeeping as a no-op. This listing compiles and leaves the lock held on both paths:

```loup,not_desired_behavior
#~ @mode(freestanding)
#~
var listLock: uint64 = 0

func lock(): void {
    # Spin until the lock word goes 0 -> 1 (the intrinsic returns 1 on success).
    while (atomicCompareExchangeU64(addressOf(listLock), 0u64, 1u64, 2) == 0u64) { }
}

func unlock(): void {
    atomicStoreU64(addressOf(listLock), 0u64, 3)
}

func removeEntry(index: uint32): bool {
    lock()
    defer { unlock() }        # accepted, but never runs in 1.25

    if (index >= 64u32) {
        return false
    }
    # ... unlink the entry ...
    return true
}
```

*Listing 4-7: A `defer` that is accepted but does not run*

> ⚠ **Planned.** Running deferred blocks at scope exit is specified but not implemented in Loup 1.25. Until it is, write the cleanup on each exit path.

The working version releases the lock explicitly before each `return`:

```loup
#~ @mode(freestanding)
#~
#~ var listLock: uint64 = 0
#~
#~ func lock(): void {
#~     while (atomicCompareExchangeU64(addressOf(listLock), 0u64, 1u64, 2) == 0u64) { }
#~ }
#~
#~ func unlock(): void {
#~     atomicStoreU64(addressOf(listLock), 0u64, 3)
#~ }
#~
func removeEntry(index: uint32): bool {
    lock()
    if (index >= 64u32) {
        unlock()
        return false
    }
    # ... unlink the entry ...
    unlock()
    return true
}
```

*Listing 4-8: Releasing a spin lock on every exit path explicitly*

The atomic intrinsics used here are covered in chapter 19. Keeping the exits few — one early return for the error case, one at the end — keeps explicit cleanup manageable.

`defer import` is a different feature that shares the keyword: a lazily resolved import, which needs a runtime module loader. It is rejected in freestanding (`E_DEFERRED_NOT_FREESTANDING`).

## Lambdas and closures

A lambda is written `func(params): ReturnType { … }`, or `func(params) => expression` for a one-line body. It can read the variables around it. To **assign** to one, the lambda must say so at the top of its body: `nonlocal name;` for a variable of an enclosing function, `global name;` for a module-level variable (Language Reference §1-7.3). Forgetting it is an error:

```loup,does_not_compile
@mode(freestanding)

func countEven(limit: uint32): uint32 {
    var evens: uint32 = 0
    let visit = func(n: uint32): void {
        if (n % 2u32 == 0u32) {
            evens = evens + 1u32
        }
    }
    var i: uint32 = 0
    while (i < limit) {
        visit(i)
        i = i + 1u32
    }
    return evens
}
```

```text
error[E_GLOBAL_MISSING]: E_GLOBAL_MISSING: assignment to outer-scope `var` `evens` inside a closure needs a `nonlocal evens` (an enclosing function's variable) or `global evens` (a module variable) declaration at the top of the closure's body (§1-7.3)
 --> src/main.lpfree:7:13
  |
7 |             evens = evens + 1u32
  |             ^^^^^^^^^^^^^^^^^^^^
  |
```

The fix is one line:

```loup
#~ @mode(freestanding)
#~
func countEven(limit: uint32): uint32 {
    var evens: uint32 = 0
    let visit = func(n: uint32): void {
        nonlocal evens;
        if (n % 2u32 == 0u32) {
            evens = evens + 1u32
        }
    }
    var i: uint32 = 0
    while (i < limit) {
        visit(i)
        i = i + 1u32
    }
    return evens
}
```

*Listing 4-9: Declaring the captured variable a lambda assigns*

A parameter typed `noescape fn(...)` promises that the callee only calls the function during the call and never stores it. That is the right shape for a visitor: the lambda's captures can live in the caller's frame, and the borrow checker knows the lambda cannot outlive them (chapter 11).

```loup
#~ @mode(freestanding)
#~
# Visit every slot index of a fixed table.
func forEachSlot(count: int, visit: noescape fn(int): void): void {
    var i = 0
    while (i < count) {
        visit(i)
        i = i + 1
    }
}

var dirtySlots: int = 0

func markAll(): int {
    forEachSlot(16, func(slot: int): void {
        global dirtySlots;
        dirtySlots = dirtySlots + 1
    })
    return dirtySlots
}
```

*Listing 4-10: A `noescape fn` visitor*

A closure that captures a `unique` value moves it into the closure (ruling CAPMV-1); chapter 12 shows what that means for ownership.

## Generators

`gen func` declares a generator that `yield`s values, and `for`-`in` consumes it:

```loup
#~ @mode(freestanding)
#~
gen func countdown(n: uint32): Iterable[uint32] {
    var i = n
    while (i > 0u32) {
        yield i
        i = i - 1u32
    }
}

func total(): uint32 {
    var sum: uint32 = 0
    for (v in countdown(3u32)) {
        sum = sum + v
    }
    return sum
}
```

*Listing 4-11: A synchronous generator*

A generator keeps its state between `yield`s, and in freestanding that state needs storage that outlives a single call. The Language Reference therefore allows `gen func` in freestanding only when the program registers an `@allocator` (the mode feature matrix, §1-2.6). The 1.25 checker does not enforce that, so the rule is yours to keep: register an allocator (chapter 7) before you use generators, or write the plain loop, which is what most kernel code does. `async gen func` is not available in freestanding at all.

## What is missing

These features need a runtime that a freestanding image does not have:

- **`async`, `await`, `Task[T]`, `spawn`.** There is no executor. The Language Reference makes them `E_FREESTANDING_ASYNC` errors. The 1.25 checker accepts a plain `async func` declaration without complaint, so do not take its silence as permission: use interrupt handlers, a polling main loop, or per-CPU work loops instead (chapters 16 and 19).
- **`throw`, `catch (T as e)`.** There is no unwinder. Both are rejected with `E_FREESTANDING_EXCEPTIONS`; chapter 9 shows `Result`, `?`, and the `try … catch Err(e)` form that *is* allowed.
- **`portal` statements** outside `--debug` builds (`E_FREESTANDING_PORTAL`); chapter 23 covers the freestanding debug service.

## Early return with `?`

A preview of chapter 9: a function that returns `Result[T, E]` can propagate an error from a call with `?`. On `Err`, `?` returns that error from the enclosing function; on `Ok(v)` it evaluates to `v`.

```loup
#~ @mode(freestanding)
#~
enum UartError {
    Timeout,
    Framing,
}
#~
#~ func uartReady(): bool { return true }
#~ func uartRead(): uint8 { return 0x41u8 }

func readByte(): Result[uint8, UartError] {
    if (uartReady()) {
        return Ok(uartRead())
    }
    return Err(UartError.Timeout)
}

func readWord(): Result[uint16, UartError] {
    let high = readByte()?
    let low = readByte()?
    return Ok(((high as uint16) << 8) | (low as uint16))
}
```

*Listing 4-12: Propagating an error with `?`*

## Summary

- `func name(params): Type { … }`; `pub` exports; default parameters and named arguments work as in every mode. `#` comments, not `//`.
- Conditions are always parenthesized (`E_CONDITION_PARENS`); `if` is an expression and there is no ternary.
- `while`, C-style `for`, `for`-`in` over ranges, strings and generators; a `buffer` is indexed, not iterated. No `loop` keyword — use `while (true)`.
- `match` is exhaustive and supports guards.
- `defer` is accepted but its block does not run in 1.25 — release resources explicitly on each exit path; `defer import` is rejected in freestanding.
- A lambda that assigns a captured variable declares it with `nonlocal` or `global` (`E_GLOBAL_MISSING`). `noescape fn` parameters take visitors that must not escape.
- `gen func` needs an `@allocator` in freestanding by rule, though the checker does not enforce it; `async` and exceptions are not available.

Next: structs and layout control.
