# Chapter 17: Entry Points and Panic

A freestanding program has two special functions. The entry point is the first Loup code the CPU runs: there is no C runtime and no `main`, only an address the boot loader or the reset vector jumps to. The panic handler is the last: it runs when the program reaches a state it cannot continue from. This chapter covers:

- `@entry` and the rules for the entry function;
- getting from a bare entry stub to an ordinary `kernelMain`;
- what counts as a panic in Loup 1.25, and what to write instead of `panic(...)`;
- the panic policy in the manifest (`[build] panic`);
- writing a `@panic_handler`.

## `@entry`

`@entry` marks the function the linker uses as the image's entry symbol (Language Reference §3-3.5). It is the root of the image: the compiler keeps the entry, every `@section` function, every `@interrupt` handler, and whatever they reach, and drops everything else.

`@entry` does not imply `@naked`. If the code that jumps to your entry has already set up a stack, a plain function works. If it has not — the usual case for a kernel's first instruction — the entry must be naked, and its assembly sets the stack pointer before calling into Loup code. Here is the entry of a kernel for QEMU's AArch64 `virt` machine, which starts the image at EL1 with no stack:

```loup
@mode(freestanding)

# QEMU `virt` (aarch64): the image is linked at 0x40080000 and entered at EL1
# with the MMU off and no stack.
@entry
@naked
@unsafe("naked: runs before any stack exists; sets sp from the linker script, then calls kernelMain")
@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"
    }
}

func kernelMain(): void {
    # ... bring up the console, the exception vectors, the timer ...
    while (true) {
        asm { "wfi" }
    }
}
```

*Listing 17-1: A naked entry stub that sets up the stack and calls `kernelMain`*

`__stack_top` is a symbol the linker script defines (chapter 18). `@section(".text.boot")` lets the linker script put this code first, at the address the loader jumps to. The loop after `bl kernelMain` is a safety net; `kernelMain` should never return.

The manifest names the same symbol for the linker:

```toml
[linker]
script = "scripts/linker.ld"
entry  = "_start"
```

If the linked image has no entry symbol, the build fails (`E-BUILD-NO-ENTRY`) instead of producing an image the CPU would jump into at an arbitrary address.

On x86_64, `qemu-system-x86_64 -kernel` starts a Multiboot2/PVH image in 32-bit protected mode, so the entry stub there also has to build page tables and switch to 64-bit long mode before it can call Loup code. Chapter 21 shows that stub in full.

### Rules for the entry

There is exactly one `@entry` per program:

```loup,does_not_compile
@mode(freestanding)

@entry
func bootPrimary(): void {
}

@entry
func bootSecondary(): void {
}
```

```text
error[E0001]: E_ENTRY_DUPLICATE: at most one `@entry` function per compilation unit; `bootSecondary` is the second `@entry` (§3-3.6)
 --> src/main.lpfree:7:1
  |
7 | @entry
  | ^^^^^^
  |
```

Secondary CPUs on a multi-core system do not get a second `@entry`. They start at an address the primary CPU gives the firmware or the interrupt controller (an AP trampoline on x86_64, PSCI `CPU_ON` on AArch64), which is an ordinary `@section` function or naked stub.

The entry must also be a module-level function; on a method `@entry` is `E_ANNOTATION_TARGET`. The Language Reference further requires it to take no parameters and return `void` (`E_ENTRY_PARAMS`, `E_ENTRY_NONVOID`); the 1.25 checker does not report those two, so keep them yourself. Boot information that a loader passes in registers (a Multiboot info pointer in `ebx`, a device tree address in `x0`) is saved by the naked stub into a module global before it calls Loup code.

The Language Reference also lets you name the exported symbol differently from the function. That form is not implemented:

```loup,does_not_compile
@mode(freestanding)

@entry(symbol: "Reset_Handler")
func reset(): void {
}
```

```text
error[E_ANNOTATION_ARGS]: E_ANNOTATION_ARGS: @entry does not take arguments
 --> src/main.lpfree:3:1
  |
3 | @entry(symbol: "Reset_Handler")
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
```

> ⚠ **Planned.** `@entry(symbol: "…")` (Language Reference §3-3.5) is rejected by the 1.25 checker. Name the function after the symbol your loader or vector table expects (`_start`, `Reset_Handler`), and give the same name to `[linker] entry`.

## From the entry to `kernelMain`

Keep the naked stub as small as possible and do everything else in ordinary Loup code. `kernelMain` can call functions, use locals and structs, and loop; it cannot return, because there is nothing to return to.

Module-level variables with initializers that are computed at run time (anything beyond a constant) are set up by a module initializer. The compiler runs it once, at the start of the function your entry stub calls, before that function's own code. So by the time `kernelMain`'s first statement runs, every module global holds its declared value. Code that runs *before* that point — the naked stub itself, or anything it calls other than `kernelMain` — must not rely on module globals with computed initializers.

## Panics in Loup 1.25

A panic is a stop the compiler inserts for a condition the program cannot continue from. In 1.25 two constructs produce one:

- reaching an `unreachable` expression;
- an out-of-range runtime index into a `buffer[T, N]` when the manifest has `[freestanding] bounds = "trap"` (chapter 23).

The Language Reference adds more panic sources — an explicit `panic("message")` call, `Option.unwrap()` on `None`, and integer overflow under `overflow = "trap"` — but they are not available in freestanding code in 1.25. In particular, `panic` itself is not defined:

```loup,does_not_compile
@mode(freestanding)

func pageFrame(index: uint64, frames: uint64): uint64 {
    if (index >= frames) {
        panic("page frame index out of range")
    }
    return index * 4096u64
}
```

```text
error[E0001]: undefined identifier `panic`
 --> src/main.lpfree:5:9
  |
5 |         panic("page frame index out of range")
  |         ^^^^^
  |
```

> ⚠ **Planned.** `panic(msg): noreturn` (Language Reference §3-3.6, stdlib reference §21.13) is not resolved by Stage0 in any mode. Overflow trapping (`overflow = "trap"`) is accepted in the manifest but has no effect (chapter 3).

What to write instead is a small `noreturn` function of your own that reports the message and then reaches `unreachable`, so the stop goes through the package's panic policy like any other panic:

```loup
#~ @mode(freestanding)
#~
#~ const UART0: uint = 0x1000_0000
#~
#~ func uartPutc(c: uint8): void {
#~     while (((Volatile[uint8].at(UART0 + 5).read() as uint8) & 0x20u8) == 0u8) { }
#~     Volatile[uint8].at(UART0).write(c)
#~ }
#~
#~ func uartPuts(s: string): void {
#~     for (c in s) {
#~         uartPutc(c as uint8)
#~     }
#~ }
#~
# Print a fatal message, then stop through the package's panic policy.
func fatal(msg: string): noreturn {
    uartPuts("fatal: ")
    uartPuts(msg)
    uartPuts("\n")
    unreachable
}

func pageFrame(index: uint64, frames: uint64): uint64 {
    if (index >= frames) {
        fatal("page frame index out of range")
    }
    return index * 4096u64
}
```

*Listing 17-2: A `fatal` helper that reports and then panics*

A `noreturn` function must not have a path that completes normally (`E_NORETURN_FALLTHROUGH`); ending it with `unreachable` satisfies that. Exceptions are not an alternative: `throw` and `catch (T as e)` are `E_FREESTANDING_EXCEPTIONS` (chapter 9).

## The panic policy

What happens at a panic is decided per package by one manifest key:

```toml
[build]
panic = "halt"     # "halt" (the default) | "abort" | "custom"
```

| Policy | What a panic does | Handler called? |
|---|---|---|
| `halt` | masks interrupts and parks the core in a wait-for-interrupt loop it never leaves | no |
| `abort` | executes the architecture's trap instruction (`ud2` on x86_64, `brk` on AArch64, `udf` on Arm, `unimp` on RISC-V) | no |
| `custom` | calls your `@panic_handler`; if the handler returned, the trap of `abort` follows | yes |

The halt loop is `cli` + `1: hlt; jmp 1b` on x86_64, `msr daifset, #0xf` + `1: wfi; b 1b` on AArch64, `csrci mstatus, 8` + `1: wfi; j 1b` on RISC-V, and `cpsid i` + `1: wfi; b 1b` on Cortex-M (Language Reference §3-3.6, ruling PANIC-1).

Three points that are easy to get wrong:

- **An absent key means `halt`** (since 1.25.0). Before that, a program with a handler got `custom` implicitly. Now **only `panic = "custom"` calls the handler**: a program that declares a `@panic_handler` and leaves the key out halts at a panic and never runs the handler, and nothing warns you.
- **There is no `--panic=` flag.** The policy lives in the manifest only.
- **The value is checked.** A misspelled policy is an error, not a silent default: `panic = "explode"` gives `invalid [build].panic 'explode'; must be one of: halt, abort, custom` (chapter 2 shows the full report).

`halt` is a good choice for production images and early board bring-up: the machine stops cleanly and does no further damage. `abort` suits debugging with an attached debugger, which stops on the trap with the faulting address and registers intact. `custom` is what you want once you have a console to report on.

## Writing a `@panic_handler`

With `panic = "custom"`, every panic calls the function marked `@panic_handler` with a message, the source file and the line of the construct that panicked. The file is relative to the directory of the entry file (`main.lpfree`, `drv/table.lpfree`). The message is fixed by the kind of panic: ``reached an `unreachable` expression`` for `unreachable`, `buffer index out of range` for a bounds guard.

The handler's signature is fixed, and its return type is `noreturn`, not `void`: returning from a panic handler would resume the code that just failed. A C or Rust habit gives `void`:

```loup,does_not_compile
@mode(freestanding)

@panic_handler
func kernelPanic(msg: string, file: string, line: int): void {
    while (true) {
        asm { "cli" "hlt" }
    }
}
```

```text
src/main.lpfree:1:1: error: E_PANIC_HANDLER_SIG: `@panic_handler` `kernelPanic` must have one of the two normative signatures — `(msg: string, file: string, line: int): noreturn` or `(msg: string, file: string, line: int, source: PanicSource): noreturn` — it returns `void`; a panic handler must be `noreturn`, because returning from it resumes into the fault it was called to report (§3-3.6)
```

(The one-line `--format short` form again: as with `E_NAKED_NOT_UNSAFE`, the 1.25 checker anchors this diagnostic at the file's first line.) Changing `void` to `noreturn` is not quite enough if the loop that never exits is written in assembly:

```loup,does_not_compile
@mode(freestanding)

@panic_handler
func kernelPanic(msg: string, file: string, line: int): noreturn {
    asm { "cli" "1: hlt" "jmp 1b" }
}
```

```text
src/main.lpfree:1:1: error: E_NORETURN_FALLTHROUGH: `kernelPanic` is declared `noreturn` but has a path that completes normally — every path must diverge (`throw`, a call to another `noreturn` function, or a loop with no reachable exit)
```

The compiler does not read assembly to decide whether a function returns, so the loop must be a Loup loop with the assembly inside it. Here is a complete handler for the RISC-V `virt` machine, which masks interrupts, reports the message, file and line on the UART, and parks the core:

```loup
#~ @mode(freestanding)
#~
#~ const UART0: uint = 0x1000_0000
#~
#~ func uartPutc(c: uint8): void {
#~     while (((Volatile[uint8].at(UART0 + 5).read() as uint8) & 0x20u8) == 0u8) { }
#~     Volatile[uint8].at(UART0).write(c)
#~ }
#~
#~ func uartPuts(s: string): void {
#~     for (c in s) {
#~         uartPutc(c as uint8)
#~     }
#~ }
#~
# Decimal output without a buffer: find the largest power of ten first.
func uartPutDec(n: uint64): void {
    var divisor: uint64 = 1u64
    while (n / divisor >= 10u64) {
        divisor = divisor * 10u64
    }
    var rest = n
    while (divisor > 0u64) {
        uartPutc((0x30u64 + rest / divisor) as uint8)
        rest = rest % divisor
        divisor = divisor / 10u64
    }
}

@panic_handler
func kernelPanic(msg: string, file: string, line: int, source: int): noreturn {
    asm { "csrci mstatus, 8" }          # RISC-V machine mode: mask interrupts
    uartPuts("\nPANIC: ")
    uartPuts(msg)
    uartPuts(" at ")
    uartPuts(file)
    uartPutc(0x3Au8)                    # ':'
    uartPutDec(line as uint64)
    uartPuts("\n")
    while (true) {
        asm { "wfi" }
    }
}
```

*Listing 17-3: A serial panic handler*

The fourth parameter is optional. When present it receives a number saying what kind of panic this is: `1` for a bounds guard, `6` for `unreachable`. The Language Reference declares these values as an enum, `PanicSource`, which does not exist yet:

> ⚠ **Planned.** `enum PanicSource { Assertion, BoundsCheck, OverflowTrap, UnwrapNone, StackOverflow, Unknown, Unreachable }` (Language Reference §3-3.6). Writing `source: PanicSource` in 1.25 gives `undefined type 'PanicSource'`; write `source: int` and compare against the variant numbers (0 to 6, in that order).

Guidelines for the handler itself:

- **Mask interrupts first.** A handler that is interrupted may never finish its report.
- **Use little stack.** The panic may have been a stack problem. No recursion, no large locals, no deep call chains — which is why `uartPutDec` above uses a loop instead of a buffer or recursion.
- **Call only code you trust not to panic.** A panic inside the handler is undefined behaviour (the Language Reference specifies a `double_fault` policy for it; the manifest key is accepted in 1.25 but has no effect). Do not index buffers with computed indexes here.
- **Write directly to the hardware.** Do not take the console lock the panicking code may be holding; go straight to the UART registers, as Listing 17-3 does.

Under `halt` and `abort` a declared handler is never called, and the image does not contain it. Under `custom` without a handler, `unreachable` stops with the trap and `bounds = "trap"` guards are not emitted at all — declare the handler whenever you set `custom`.

## Summary

- `@entry` marks the single entry function; it is the image's reachability root. One per program (`E_ENTRY_DUPLICATE`), module level only (`E_ANNOTATION_TARGET`), no parameters and `void` (by rule; not checked in 1.25).
- An entry that runs without a stack is `@naked` + `@unsafe` and sets the stack pointer before calling `kernelMain`; `[linker] entry` names the same symbol. `@entry(symbol: …)` is planned.
- Module initializers run once at the start of the function the entry calls.
- In 1.25, panics come from `unreachable` and `bounds = "trap"` guards. `panic()` is undefined; write a `noreturn` helper that reports and reaches `unreachable`.
- `[build] panic = "halt" | "abort" | "custom"`; absent means `halt`; only `custom` calls the handler; there is no `--panic=` flag.
- `@panic_handler func h(msg: string, file: string, line: int): noreturn`, optionally with `source: int`. `void` is `E_PANIC_HANDLER_SIG`; the body must diverge with a Loup loop (`E_NORETURN_FALLTHROUGH`). `PanicSource` is planned.

Next: sections and the linker.
