# Chapter 1: What Is Freestanding Mode?

**What you'll learn.** What `@mode(freestanding)` is, what it keeps from the rest of Loup, what it leaves out, what the 1.25 toolchain can do with it today, and what a first freestanding program looks like.

Loup has six execution modes. Five of them run on top of something — an operating-system process or a browser — and the toolchain supplies a runtime underneath your code. The sixth, `freestanding`, runs where nothing is underneath: a kernel, a boot loader, firmware on a microcontroller, a hypervisor payload.

## The six modes at a glance

| Mode | Runs on | How it runs | Typical file |
|---|---|---|---|
| `@mode(host.scripted)` | an OS process | interpreted; top-level statements run in order | `tool.lpscr` |
| `@mode(host.dual)` | an OS process | interpreted, or compiled ahead of time; `main()` | `app.loup` |
| `@mode(host.native)` | an OS process | compiled to a native binary; `main()` | `server.lphc` |
| `@mode(web.scripted)` / `@mode(web.compiled)` | a browser | planned browser runtime | `page.lpweb` |
| `@mode(freestanding)` | bare metal | compiled to a native image with **no Loup runtime**; an `@entry` function | `kernel.lpfree` |

The other books cover the hosted modes: [The Loup Scripting Book](../loup-scripting/), [The Loup Native Book](../loup-native/) and [The Loup Web Book](../loup-web/). Freestanding code is written in `.lpfree` files, and every file starts with `@mode(freestanding)`.

A freestanding image contains only the code you wrote, the code it reaches, and a few compiler helpers (`memcpy`, `memset` and the like) that the toolchain links automatically. There is no garbage collector or reference counter, no exception unwinder, no async executor, no libc, and no `runtime` object to ask about the environment. When the first instruction of your image runs, the CPU's state is whatever the firmware or boot loader left it in — often with no stack set up at all.

## The boot story

Something hands your image control: GRUB or another Multiboot loader, UEFI firmware, a hypervisor, or the reset vector of a microcontroller. The hand-off happens at a known address, with a known register state, under a calling convention someone else chose.

From there, your code does everything. It sets the stack pointer. It sets up interrupt vectors. It finds out what memory exists, builds page tables, talks to the UART, the timer and the interrupt controller, and drops into a loop that never returns — there is nothing to return to.

Writing that code in Loup is what this book is about.

## Why use Loup here rather than C, Rust or Zig?

Consider a network driver that fills a transmit buffer and hands it to the device. In C, nothing records that the CPU must not touch the buffer afterwards:

```c
struct tx_buf { uint8_t bytes[1514]; uint32_t len; };

void transmit(struct tx_buf *buf) {
    nic_post_descriptor(buf);   /* the device now owns the bytes */
    buf->len = 0;               /* compiles; races with the device's DMA */
}
```

Rust expresses the hand-off with ownership, and so does Loup — with ownership *verbs* written where the reader looks for them. A `takes` method consumes its receiver, so after the hand-off the old binding cannot be used:

```loup
@mode(freestanding)

unique class TxBuffer {
    pub var frame: uint64;          # physical address of the bytes
    pub var length: uint32;

    new(frame: uint64, length: uint32) {
        this.frame = frame
        this.length = length
    }

    # Gives the buffer to the device and returns the descriptor value to post.
    takes func handToDevice(): uint64 {
        return this.frame | (this.length as uint64)
    }
}

func transmit(): uint64 {
    let tx = TxBuffer(0x20_0000u64, 1514u32)
    let descriptor = tx.handToDevice()
    # tx.length = 0     -- would be E_USE_AFTER_TAKE: `tx` was consumed
    return descriptor
}
```

*Listing 1-1: Ownership hand-off expressed as a `takes` method*

The check happens at compile time and costs nothing at run time. The same machinery — `reads`/`edits`/`takes` verbs, `unique` values, lifetime-labelled borrows — works all the way down to device registers, and freestanding adds hardware features on top: inline assembly, volatile MMIO, interrupt handlers, naked functions, layout control and linker sections. Chapters 10–12 cover ownership; chapters 13–18 cover the hardware features.

## What you keep

- **The type system**: fixed-width integers, `bool`, `string` (as an immutable slice), structs, enums, union types, tuples, generics, interfaces with compile-time dispatch, exhaustive `match`.
- **Ownership and borrowing**: the verbs `reads`/`edits`/`takes`, `modifier`, `safe` contexts, `unique` classes and bindings, `borrow`/`excl borrow`, lifetimes (`[lifetime ~a]`, `borrow[~a]`), `noescape fn`, `unowned[~a]`.
- **Error handling with values**: `Result[T, E]`, `?`, `From` conversions, and `try { … attempt … } catch Err(e) { … }`.
- **Compile-time evaluation**: `const`, `const func`, `if const`, `size_of[T]()`, `align_of[T]()`.

And you gain the freestanding features: `@entry`, `@naked`, `@callconv`, `asm { }`, `Volatile[T]` and `volatile borrow`, `@repr`, `@align`, `@section`, `buffer[T, N]` with overlays, `onstack`, `@interrupt`, `@interruptSafe` and `InterruptGuard`, `@panic_handler` with a panic policy, `@allocator`/`@deallocator`, `PartialInit[T]`, and a set of bare intrinsics for atomics, raw memory and interrupt masking.

## What you do not have

These features need a runtime that a freestanding image does not contain. The Language Reference assigns each an error code (§3-3.3); the second column says whether the 1.25 checker reports it today.

| Not available | Reported by `loup check` in 1.25 |
|---|---|
| `throw` and `catch (T as e)` — use `Result` | yes: `E_FREESTANDING_EXCEPTIONS` |
| `weak` references | yes: `E_FREESTANDING_WEAK` |
| `dynamic class` | yes: `E_FREESTANDING_DYNAMIC` |
| `defer import` | yes: `E_DEFERRED_NOT_FREESTANDING` |
| `consume` (unique-to-ARC downgrade) | yes: `E_CONSUME_NOT_AVAILABLE` |
| hosted standard-library modules (`core.math`, `system.*`, …) | yes: the import is rejected |
| `async`/`await`, `Task[T]`, `spawn` | partly — `await` outside an `async func` is rejected, but an `async func` declaration is not |
| `bigint`, `decimal`, the `runtime` object | no — don't use them |
| template fences, reflection, `ui.*` | no — don't use them |
| growable collections (`list`, `dict`) | no — nothing enforces it; use `buffer[T, N]` (chapter 13) |
| class construction without an `@allocator` | no — and see chapter 7 for where objects actually live |

"No" in the second column means the code may type-check and then fail to build, or build into something that cannot work on bare metal. Treat the whole table as rules you keep, not as a safety net.

A natural first mistake is to reach for the standard library:

```loup,does_not_compile
@mode(freestanding)

from core.math import (sqrt);

func diagonal(w: float, h: float): float {
    return sqrt(w * w + h * h)
}
```

```text
error[E0001]: `@mode(freestanding)` module cannot import `@mode(host.scripted)` module
 --> src/main.lpfree:3:1
  |
3 | from core.math import (sqrt);
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
```

`core.math` is a hosted module (the message names the mode the checker resolved it under). In 1.25 the only standard-library modules a freestanding file can import are the debugging and test helpers `debug.freestanding`, `debug.assert_fs` and `debug.recording`, plus `native.css` for the planned s390x target; chapter 20 lists what is built in without an import. Kernel code computes with integers and writes the few routines it needs itself.

## What the 1.25 toolchain does with freestanding code

- **Type-checking** works everywhere the toolchain runs: `loup check` in a project directory (on Linux x86-64, where `loup` is the self-hosted driver, the command is `loup-bootstrap check`). Every listing in this book is checked that way.
- **Building an image** — `loup build --target freestanding --arch …` — works on **Linux x86-64**, needs `ld.lld`, and uses the clang-free `llbe` back end by default (chapter 2). Freestanding builds are not available on macOS or Windows.
- **Targets**: images for x86_64, aarch64, riscv64, armv7 and Cortex-M (`thumbv8.1m.main` and relatives) are boot-tested under QEMU. riscv32 boots through the LLVM back end. loongarch64, mips64el and powerpc64le are encoder-level only (no image has been booted), x86 (32-bit) goes through the C/LLVM path only, and s390x is specified but not implemented.
- The project's own status table calls freestanding **experimental**. Expect gaps; this book marks the ones a reader would hit.

## A first freestanding program

This program prints a line on the serial console of QEMU's AArch64 `virt` machine, whose PL011 UART is memory-mapped at `0x0900_0000`:

```loup
#~ @mode(freestanding)
#~
# QEMU `virt` (AArch64): the PL011 UART's data register.
let UART0_DATA = Volatile[uint8].at(0x0900_0000)

func uartWrite(s: string): void {
    for (c in s) {
        UART0_DATA.write(c as uint8)
    }
}

func kernelMain(): void {
    uartWrite("Hello from Loup freestanding\n")
    while (true) {
        asm { "wfi" clobbers: [] }
    }
}

# The first code that runs: give the CPU a stack, then call into Loup.
@entry
@naked
@unsafe("naked: runs before any stack exists; sets sp from __stack_top")
@section(".text.boot")
func _start(): void {
    asm {
        "adrp x0, __stack_top"
        "add x0, x0, :lo12:__stack_top"
        "mov sp, x0"
        "bl kernelMain"
        "1: wfe"
        "b 1b"
        clobbers: ["memory"]
    }
}
```

*Listing 1-2: `src/main.lpfree` — hello world over a memory-mapped UART*

Things to notice:

1. The file starts with `@mode(freestanding)`. Every freestanding source file declares its mode on its first line (ruling MODE-2).
2. `Volatile[uint8].at(addr)` wraps a device register; every `write` is a real store the optimizer will not remove or merge (chapter 15).
3. `@entry` names `_start` as the image's entry point. There is no `main`.
4. `_start` runs before any stack exists, so it must not have a compiler-generated prologue: `@naked` removes it, and a naked function's body may contain only `asm` (chapter 16). `@section(".text.boot")` places it where the linker script expects the first instruction (chapter 18).
5. Nothing returns. `kernelMain` ends in a `while (true)` loop, and `_start` parks the core if it ever comes back.

`@naked` is the one feature in this program that demands an audit trail. Leave out the `@unsafe` line and the checker stops you:

```text
src/main.lpfree:9:1: error: E_NAKED_NOT_UNSAFE: `@naked` function `_start` must also carry `@unsafe("reason")` — naked functions have no stack frame and are inherently unsafe (§3-3.6)
```

(That is the one-line form `loup check --format short` prints, for a variant of Listing 1-2 without the `@unsafe` line.) The reason string is free text, written for the next person who reads the code.

On x86 PCs the classic COM1 serial port is different: it lives in the separate *I/O-port* space at port `0x3F8` and is written with the `out` instruction, not through a memory address (chapter 14 shows `outb`). Writing `Volatile[uint8].at(0x3F8)` on x86 would store into RAM at address `0x3F8`.

Chapter 2 adds the linker script and manifest, builds this program and boots it in QEMU.

## How this book is organized

- **Part I (chapters 1–4)**: the mode, building and booting, integers, functions and control flow.
- **Part II (5–8)**: data layout, enums, classes and allocators, interfaces and generics.
- **Part III (9–13)**: error handling without exceptions, ownership verbs, borrows and lifetimes, `unique`, memory primitives.
- **Part IV (14–18)**: inline assembly, MMIO, interrupts and naked functions, entry points and panics, sections and linker scripts.
- **Part V (19–24)**: multi-core and atomics, the available library surface, a complete x86_64 kernel stub, testing and deployment, kernel safety patterns, and `synchronized` classes.

## Summary

- `@mode(freestanding)` compiles `.lpfree` files into a native image with no Loup runtime; the entry point is an `@entry` function, not `main`.
- You keep the type system, ownership and borrowing, `Result`-based errors and compile-time evaluation, and gain inline assembly, volatile MMIO, interrupts, layout control and linker sections.
- Exceptions, `weak`, `dynamic class`, `defer import`, `consume` and hosted library modules are rejected; `async`, `bigint`, collections and several others are not yet rejected but do not belong in freestanding code.
- `loup check` works everywhere; image builds need Linux x86-64 and `ld.lld`; x86_64, aarch64, riscv64, armv7 and Cortex-M images boot under QEMU.

Next: building a bare-metal binary.
