# Chapter 25: Memory safety patterns

Chapters 15 to 20 introduced Loup's safety mechanisms one at a time: ownership verbs, borrows, `unique`, lifetimes, `@unsafe` and `Sendable`. This chapter goes the other way. It takes the classic memory-safety bugs of C and C++ one by one and shows, for each, which Loup mechanism addresses it, what the 1.25.1 compiler actually catches, and what is still your responsibility. Every diagnostic below is real; where a specified check is missing, the chapter shows the program that slips through.

| Bug class | Loup's defence | Status in 1.25.1 |
|---|---|---|
| Use after free, double free | no manual `free`; ARC; `unique` move tracking; `del` | checked at compile time |
| Dangling reference | lifetimes, inferred struct lifetimes, `unowned[~name]` | partly checked (chapter 18); plain `borrow` escapes are not |
| Iterator invalidation | the specified loop-borrow rule (`E_BORROW_LOOP_MUTATION`) | not checked |
| Data race | `Sendable`, `synchronized`, `Mutex`/`Atomic` | checked, with one gap |
| Out-of-bounds access | bounds checks on every index | checked at run time (`IndexError`) |
| Null dereference | no `null`; `Option[T]` | checked at compile time; a non-exhaustive `match` is caught only at run time |
| Uninitialized read | definite assignment | checked for class-typed locals |
| Partly built struct | `PartialInit[T]` | checked |
| Type punning | `reinterpret[T, U]` with `@unsafe` | ⚠ planned |
| Integer overflow | checked arithmetic, `@overflowWrapping` | partly; see chapter 3 |

## Use after free and double free

C code frees memory explicitly, so it can free too early or twice. Loup has no `free`. An ordinary class instance lives as long as any reference to it (ARC, chapter 9); a `unique` value has exactly one owner and the compiler tracks where it goes (chapter 17). The one explicit end-of-life operation, `del`, is tracked like a move:

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

unique class TempFile {
    pub let path: string;
    new(path: string) { this.path = path; }
    func finalize(): void { println(i"removing {$ this.path $}"); }
}

func cleanup(f: TempFile): void {
    println(i"cleaning {$ f.path $}");
}

func main(): void {
    let scratch = TempFile("/tmp/build-7");
    del scratch;
    cleanup(scratch);
}
```

```text
main.lphc:16:13: error[E_USE_AFTER_DEL]: E_USE_AFTER_DEL: use of binding 'scratch' after `del` — the binding is uninitialized once it has been passed to `del` (§1-6.11)
```

Deleting twice is the same error at the second `del`. Do the work first, then end the value's life:

```loup
#~ @mode(host.native)
#~
#~ unique class TempFile {
#~     pub let path: string;
#~     new(path: string) { this.path = path; }
#~     func finalize(): void { println(i"removing {$ this.path $}"); }
#~ }
#~
#~ func cleanup(f: TempFile): void {
#~     println(i"cleaning {$ f.path $}");
#~ }
#~
func main(): void {
    let scratch = TempFile("/tmp/build-7");
    cleanup(scratch);
    del scratch;
}
```

*Listing 25-1: Ending a value's life after its last use*

```text
cleaning /tmp/build-7
removing /tmp/build-7
```

The other ways a `unique` value can be given away (assignment, `takes:`, insertion into a collection, capture by a closure) are tracked the same way: `E_USE_AFTER_MOVE` and `E_USE_AFTER_TAKE`, including across loop iterations (chapter 17).

## Dangling references

Returning a pointer to a local variable is the C version. In Loup, references that must outlive a call carry a lifetime, and the compiler checks the cases chapter 18 covers: views returned or stored without a lifetime (`E_INFERRED_LIFETIME_RETURN`, `E_INFERRED_LIFETIME_ESCAPE`), back references that outlive their target (`E_UNOWNED_LIFETIME_MISMATCH`), and borrows captured by escaping closures, threads or `async` functions.

The specified check for a *plain* `borrow` that escapes (`E_BORROW_ESCAPE`) is not implemented, so this compiles although the Language Reference forbids it:

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

func greeting(): borrow string {
    let text = "hello, " + "world";
    return borrow text;
}

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

```text
hello, world
```

`greeting` returns a borrow of its own local. Do not write this: a borrow is a promise that someone else keeps the value alive, and here nobody does. Return the value itself (`func greeting(): string`), or, if the result really is part of an argument, declare a lifetime as in Listing 18-1.

## Iterator invalidation

In C++, appending to a vector while iterating over it can reallocate the storage under the iterator. A Loup list does not crash, but changing it during a `for` loop is still a bug: the loop sees the elements you add.

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

func main(): void {
    var jobs = ["build", "test"];
    var steps = 0;
    for (job in jobs) {
        steps = steps + 1;
        if (steps > 6) {
            break;
        }
        jobs.push(job + ":retry");
    }
    println(steps);
    println(jobs.length());
}
```

```text
7
8
```

The intent was one retry per original job. Without the `break`, the loop would never end, because each retry it appends is itself visited. The specification makes this a compile-time error (`E_BORROW_LOOP_MUTATION`, §3-2.4.8); 1.25.1 does not report it. Decide up front how many elements you are iterating over:

```loup
#~ @mode(host.native)
func main(): void {
    var jobs = ["build", "test"];
    let originals = jobs.length();
    for (i in 0..originals) {
        jobs.push(jobs[i] + ":retry");
    }
    println(jobs);
}
```

*Listing 25-2: Iterating over a fixed count while appending*

```text
["build", "test", "build:retry", "test:retry"]
```

Building a new list and assigning it afterwards works too, and is often clearer.

## Data races

Two threads that write the same memory without coordination corrupt it. Loup's rule is that a thread closure may capture only `Sendable` values (chapter 20), and an ordinary class is not `Sendable`. The compiler enforces the rule, with one gap worth knowing: when the thread is started inside a `for` loop over a collection, the capture is not checked.

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

class Cache {
    var entries: dict[string, int] = {};

    pub modifier func put(key: string, value: int): void {
        this.entries[key] = value;
    }

    pub func size(): int {
        return this.entries.length();
    }
}

func main(): void {
    let cache = Cache();
    var workers: list[Thread] = [];
    for (prefix in ["a", "b", "c"]) {
        workers.push(Thread.spawn(func(): void {
            for (i in 0..100) {
                safe { cache.put(prefix + str(i), i); }
            }
        }));
    }
    for (w in workers) {
        w.join();
    }
    println(cache.size());
}
```

```text
300
```

Under `loup run` this prints the right answer, because the interpreter runs one thread at a time. In a native binary three threads write one dictionary at the same time: a data race, with lost entries or a crash as the possible results. Move the `Thread.spawn` out of the loop body (into a helper function, say) and the compiler reports `E_NON_SENDABLE_CAPTURE: captured variable 'cache' of type 'Cache' does not implement Sendable`. The fix is to make the class safe to share. A `synchronized class` takes a per-instance lock in every `modifier` method (chapter 15):

```loup
#~ @mode(host.native)
synchronized class Cache implements Sendable {
    var entries: dict[string, int] = {};

    pub modifier func put(key: string, value: int): void {
        this.entries[key] = value;
    }

    pub modifier func size(): int {
        return this.entries.length();
    }
}

func main(): void {
    let cache = Cache();
    var workers: list[Thread] = [];
    for (prefix in ["a", "b", "c"]) {
        workers.push(Thread.spawn(func(): void {
            for (i in 0..100) {
                cache.put(prefix + str(i), i);
            }
        }));
    }
    for (w in workers) {
        w.join();
    }
    println(cache.size());
}
```

*Listing 25-3: A cache that several threads may write*

```text
300
```

`size` is a `modifier` too, so that it reads under the lock, and the calls need no `safe { }` block because the lock, not a compile-time proof, provides the exclusion. For a single number, an `Atomic[int]` field is cheaper than a lock (chapter 20).

## Out-of-bounds access

Every index into a `list`, `string` or `bytes` is checked. An index past the end throws instead of reading or writing neighbouring memory:

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

func main(): void {
    let ports = [80, 443, 8080];
    let choice = 3;
    println(ports[choice]);
}
```

```text
Uncaught IndexError: list index 3 out of range (len 3)
  at main (main.lphc:6:13)
```

A native binary prints the same exception as a single line and exits with status 1. Check the index where it comes from outside the program, or catch `IndexError` (chapter 12):

```loup
#~ @mode(host.native)
func portAt(ports: list[int], choice: int, fallback: int): int {
    if (choice < 0 or choice >= ports.length()) {
        return fallback;
    }
    return ports[choice];
}

func main(): void {
    let ports = [80, 443, 8080];
    println(portAt(ports, 3, 80));
}
```

*Listing 25-4: Validating an index before using it*

```text
80
```

For `list`, `get(i)` behaves like `[i]` and throws; for `dict`, `get(key)` returns an `Option`.

## Absent values instead of null

Loup has no `null` (`undefined identifier 'null'`), and a class-typed binding cannot hold `None` (`cannot assign 'None' to binding 'v' of type 'User'`). A value that may be missing has type `Option[T]`, and the compiler stops you from using it as if it were there:

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

class User {
    pub let name: string;
    new(name: string) { this.name = name; }
}

func findUser(users: list[User], id: int): Option[User] {
    if (id < 0 or id >= users.length()) {
        return None;
    }
    return Some(users[id]);
}

func main(): void {
    let users = [User("ada"), User("grace")];
    let u = findUser(users, 5);
    println(u.name);
}
```

```text
main.lphc:18:13: error[E0001]: cannot access member 'name' on Option[User] directly; unwrap or pattern-match first
```

Handle both cases with `match`, or use `?.` and `??` for a default:

```loup
#~ @mode(host.native)
#~
#~ class User {
#~     pub let name: string;
#~     new(name: string) { this.name = name; }
#~ }
#~
#~ func findUser(users: list[User], id: int): Option[User] {
#~     if (id < 0 or id >= users.length()) {
#~         return None;
#~     }
#~     return Some(users[id]);
#~ }
#~
func main(): void {
    let users = [User("ada"), User("grace")];
    match findUser(users, 1) {
        Some(u) => println(u.name),
        None => println("no such user"),
    }
    println(findUser(users, 5)?.name ?? "unknown");
}
```

*Listing 25-5: Handling a missing value*

```text
grace
unknown
```

Two run-time failures remain possible. `unwrap()` on `None` throws `UnwrapError: called `unwrap()` on a None value`, so keep it for values you have just checked. And a `match` on an `Option` that leaves out the `None` arm compiles in 1.25.1, although matches are specified to be exhaustive; it throws `MatchError: No matching arm for value in match expression` when the value is `None`. Always write both arms.

## Uninitialized values

A `var` may be declared without a value and assigned later, but it must be assigned on every path before it is read. For class-typed variables the compiler checks this:

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

class Config {
    pub let port: int;
    new(port: int) { this.port = port; }
}

func main(): void {
    var cfg: Config;
    if (runtime.args().length() > 0) {
        cfg = Config(8080);
    }
    println(cfg.port);
}
```

```text
main.lphc:13:13: error[E_USE_BEFORE_ASSIGN]: E_USE_BEFORE_ASSIGN: `cfg` is read before it has been assigned on every path reaching this point. A `var` of class type declared without an initializer holds no instance — assign one (`cfg = T(...);`) before reading it, or declare it with its initializer
  note: §1-6.2: a `var` declared without an initializer must be assigned before its value is read. The §1-10.1.1 default-initialization exception covers struct-typed locals only, not `cfg`'s class type
```

Assign on the other path too, or compute the value with an `if` expression and bind it with `let`:

```loup
#~ @mode(host.native)
#~
#~ class Config {
#~     pub let port: int;
#~     new(port: int) { this.port = port; }
#~ }
#~
func main(): void {
    let cfg = if (runtime.args().length() > 0) { Config(8080) } else { Config(80) };
    println(cfg.port);
}
```

*Listing 25-6: Initializing on every path with an `if` expression*

```text
80
```

> **Known gap.** The same check is not applied to variables of primitive type yet: `var port: int;` followed by `println(port)` compiles and prints an empty line. Give scalar variables an initial value.

> ⚠ **Planned.** `MaybeUninit[T]` (§3-3.17), the explicit wrapper for memory a C function or a device fills in, is specified for `host.native` and `freestanding`. 1.25.1 has only a non-generic placeholder class of that name in the prelude, with none of the specified API (`MaybeUninit.uninit()`, `rawPtr()`, an `@unsafe("init: …")`-guarded `assumeInit()`).

## Building a struct field by field

C code that fills a struct one field at a time can forget a field. When a struct has to be filled incrementally, for example a protocol header assembled from several sources, `PartialInit[T]` tracks which fields have been set, at compile time (§3-3.17a):

```loup
#~ @mode(host.native)
@repr(C)
struct UdpHeader {
    var srcPort: uint16;
    var dstPort: uint16;
    var length: uint16;
    var checksum: uint16;
}

func header(src: uint16, dst: uint16, payload: uint16): UdpHeader {
    var h = PartialInit[UdpHeader].begin();
    h.set(.srcPort, src);
    h.set(.dstPort, dst);
    h.set(.length, payload + 8);
    h.set(.checksum, 0);
    return h.complete();
}

func main(): void {
    let h = header(5353, 53, 32);
    println(i"{$ h.srcPort $} -> {$ h.dstPort $}, {$ h.length $} bytes");
}
```

*Listing 25-7: Assembling a header with `PartialInit`*

```text
5353 -> 53, 40 bytes
```

`.srcPort` is a field reference, valid only as the first argument of `set`. Leave a field out and `complete()` is rejected:

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

@repr(C)
struct UdpHeader {
    var srcPort: uint16;
    var dstPort: uint16;
    var length: uint16;
    var checksum: uint16;
}

func header(src: uint16, dst: uint16): UdpHeader {
    var h = PartialInit[UdpHeader].begin();
    h.set(.srcPort, src);
    h.set(.dstPort, dst);
    return h.complete();
}

func main(): void {
    println(header(5353, 53).dstPort);
}
```

```text
main.lphc:15:12: error[E_PARTIAL_INIT_INCOMPLETE]: E_PARTIAL_INIT_INCOMPLETE: `.complete()` called with 2 unset field(s): length, checksum
```

For most structs the generated constructor is simpler and just as safe: `UdpHeader(srcPort = …, …)` with a field left out is `missing required field`.

## Reinterpreting bits

> ⚠ **Planned.** `reinterpret[T, U](value)` (§3-3.18), which reinterprets a value's bits as another type of the same size under `@unsafe("transmute: …")`, and its checked form `reinterpretChecked[T, U]`, which returns a `Result`, are accepted by the 1.25.1 parser but not implemented: both return their argument unchanged, with the source type. Do not use them yet.

## Integer overflow

`int` arithmetic wraps silently in 1.25.1 on every path, although the specification says it should throw, and the fixed-width types wrap in native binaries but throw under `loup run`. Chapter 3 has the details and the `@overflowWrapping` annotation for code that wants wrapping on purpose. Until checked `int` arithmetic lands, test the bounds of counters that could get large.

## The `@unsafe` audit

Everything above is either checked or plainly marked as unchecked. The remaining claims the compiler cannot verify are the ones you write down yourself with `@unsafe("category: reason")` (chapter 19). They are found with one search:

```sh
grep -rnE '@?unsafe\(' src/
```

Each hit should name its category (`threadSafe:`, `escapingBorrow:`, `sendable:`, `asm:`, …) and say what makes the claim true. In code review, treat a new hit as a request to check that reason.

## Translating C and C++ habits

| C / C++ | Loup |
|---|---|
| `T*` that owns its object | a class instance (ARC), or a `unique let` binding |
| `std::unique_ptr<T>` | `unique let` / `unique var`, or a `unique class` |
| `std::shared_ptr<T>` | an ordinary class instance |
| `std::weak_ptr<T>` | a `weak var` field (strong in native builds for now) or `unowned[~name]` |
| `const T&` parameter | a parameter with no verb (`reads`), or `borrow` |
| `T&` parameter that is changed | `edits` |
| `T&&` / `std::move` | `takes` and `takes:` |
| a pointer that is stored | `shares` and `shares:` |
| `std::mutex` plus the data it guards | a `synchronized class`, or a `Mutex` field |
| `std::atomic<int>` | `Atomic[int]` |
| `std::optional<T>` / nullable pointer | `Option[T]` |
| `std::variant` | an `enum` with payloads |
| `extern "C"` | `extern native` |
| `memcpy`, `reinterpret_cast` | ⚠ planned (`native.mem`, `reinterpret`) |

## Choosing how to pass a value

For each parameter:

1. Does the function only look at the value? Write no verb (`reads`). Add `borrow` on hot paths where zero-cost passing should be part of the contract.
2. Does it change the caller's value in place? `edits`.
3. Does it keep a reference after it returns (in a field, a collection, a closure)? `shares`.
4. Does it take the value over, so the caller is done with it? `takes`.

For a return value: return owned values by default; return `borrow[~a] T` only when the result is part of an argument and copying it matters.

For a class: a plain `class` by default; a `unique class` when there must never be two references; a `synchronized class` (or `Mutex`/`Atomic` fields with `implements Sendable`) when threads share it.

## Testing what the compiler cannot check

- Run the test suite on both engines: `loup test` on the interpreter and `loup test --native` as native binaries (chapter 24). The differences between them (overflow, finalization timing, real parallelism) are exactly where interpreter-only testing misses bugs.
- Write tests that run threads hard against shared objects. Under `loup run` threads take turns, so a race shows up only in a native build.
- Keep `@unsafe` code small and test it directly; it is the part of the program the compiler has agreed not to check.

## Summary

- Use-after-free and double-free cannot be written: there is no `free`, ARC keeps shared objects alive, and `unique` moves and `del` are tracked (`E_USE_AFTER_MOVE`, `E_USE_AFTER_TAKE`, `E_USE_AFTER_DEL`).
- Lifetimes catch most dangling references; plain `borrow` escapes (`E_BORROW_ESCAPE`) are not checked yet.
- Changing a list while a `for` loop walks it is not caught; iterate over a fixed count or build a new list.
- `Sendable` stops most data races, but a thread spawned inside a `for` loop over a collection escapes the check; use `synchronized` classes and `Mutex`/`Atomic` for shared state.
- Indexes are bounds-checked (`IndexError`); absent values are `Option[T]`, and member access on an `Option` does not compile.
- Class-typed variables must be assigned before use (`E_USE_BEFORE_ASSIGN`); scalars are not checked yet.
- `PartialInit[T]` checks field-by-field construction; `MaybeUninit[T]` and `reinterpret` are planned.
- `@unsafe` marks everything else, and one `grep` finds it.

For the same patterns in kernel and firmware code, see *The Loup Freestanding Book*, chapter 23: [Memory safety and kernel patterns](../loup-freestanding/ch23-memory-safety-and-kernel-patterns.md).
