# Chapter 13: Memory Primitives

Sooner or later freestanding code works on memory directly: a page has to be zeroed before it is mapped, a descriptor ring has to sit at an address the device can find, a register block is a run of words at a fixed physical address. This chapter covers the primitives Loup 1.25 gives you for that:

- `buffer[T, N]` — fixed-size blocks, either allocated by the compiler or laid over an address you supply;
- `onstack` — per-invocation storage for aggregate locals;
- the raw intrinsics `makeBufferU64`, `bufGetU64`/`bufSetU64` and friends, and how to audit them;
- `addressOf`, `size_of` and `align_of`;
- `PartialInit[T]` — building a hardware structure field by field with compile-time tracking;

and what the Language Reference specifies but 1.25 does not implement yet: `MaybeUninit[T]`, `reinterpret[T, U]`, and the `native.mem`/`native.ffi` modules.

## `buffer[T, N]` — fixed-size blocks

`buffer[T, N]` is `N` elements of type `T` with no header, no length field and no reference count — the counterpart of a C `T[N]` array (Language Reference §3-3.17b). `N` is any compile-time integer: a literal, a `const`, or arithmetic over them and `size_of`.

Declared without an initializer, a buffer is **allocated**: the compiler reserves the storage and zero-fills it.

```loup
@mode(freestanding)

const PAGES = 256
const WORDS = PAGES / 64

# One bit per physical page: 1 = free.
var freeMap: buffer[uint64, WORDS];

func markFree(page: int): void {
    let word = page / 64
    let bit = (page % 64) as uint64
    freeMap[word] = freeMap[word] | (1u64 << bit)
}

func isFree(page: int): bool {
    return (freeMap[page / 64] & (1u64 << ((page % 64) as uint64))) != 0u64
}
```

*Listing 13-1: A page bitmap in a module-level buffer*

Elements are `size_of[T]()` bytes apart — a `buffer[uint8, 16]` is 16 bytes. (The Language Reference's "storage model" paragraph in §3-3.17b still describes word-wide slots; the implementation indexes at the element size, which is what overlays of byte and 32-bit registers need.) A buffer has no methods: it is indexed with `b[i]`, an `int`, and that is all — no `.length`, no `.push`, and no `for`-`in` iteration (chapter 4).

### Index checks

A **constant** index outside `0..N` is a compile error:

```loup,does_not_compile
@mode(freestanding)

# Free-page bitmap: 4 x 64 bits = 256 pages.
func freePageCount(): uint64 {
    var bitmap: buffer[uint64, 4];
    bitmap[0] = 0xFFFF_FFFF_FFFF_FFFFu64
    bitmap[4] = 0x0Fu64
    return bitmap[0] & 0xFFu64
}
```

```text
error[E_BUFFER_INDEX_RANGE]: buffer index 4 is out of range for buffer[T, 4] (E_BUFFER_INDEX_RANGE; valid 0..4)
 --> src/main.lpfree:7:5
  |
7 |     bitmap[4] = 0x0Fu64
  |     ^^^^^^^^^
  |
```

"valid 0..4" is a half-open range: indexes 0 through 3. A **run-time** index is not checked by default — an out-of-range one reads or writes the neighbouring memory, exactly like C. Turn on run-time checks per package:

```toml
[freestanding]
bounds = "trap"     # "off" (the default) emits no check
```

With `bounds = "trap"`, every non-constant subscript of a buffer whose size is visible in its declared type is guarded, and a bad index is a panic under the package's `[build] panic` policy — halt by default, or your `@panic_handler` with the message `buffer index out of range` under `panic = "custom"` (chapter 17).

### Buffer parameters

A buffer parameter has an exact size, and an argument must match it — a `buffer[uint64, 16]` is not a `buffer[uint64, 8]`:

```loup,does_not_compile
@mode(freestanding)

func checksum(block: buffer[uint64, 8]): uint64 {
    var sum: uint64 = 0
    for (var i = 0; i < 8; i = i + 1) {
        sum = sum ^ block[i]
    }
    return sum
}

func checkLargeBlock(): uint64 {
    var block: buffer[uint64, 16];
    return checksum(block)
}
```

```text
error[E0001]: argument 1 of 'checksum': expected 'buffer[uint64, 8]', found 'buffer[uint64, 16]'
  --> src/main.lpfree:13:21
   |
13 |     return checksum(block)
   |                     ^^^^^
   |
```

This is what makes `bounds = "trap"` sound for parameters: the declared `N` is the bound. Write the function for the size it is used with, or pass the size as a separate argument and use the raw intrinsics below.

## Overlays — a buffer laid over an address

Initialized with an integer, a buffer does not allocate: it **views** the memory at that address. This is how freestanding code treats a register page, a DMA area or a scratch page as an array. Because the compiler cannot check that the address is valid, an overlay needs an explicit acknowledgment. Without one, 1.25 compiles the code but warns:

```loup,not_desired_behavior
@mode(freestanding)

# The local APIC registers, identity-mapped at their reset address.
var lapic: buffer[uint32, 1024] = 0xFEE0_0000

func lapicId(): uint32 {
    return lapic[8] >> 24          # ID register at byte offset 0x20
}
```

```text
warning[W_OVERLAY_NOT_UNSAFE]: W_OVERLAY_NOT_UNSAFE: `lapic` is an overlay — it views the memory at an integer address as a buffer, which the compiler cannot check (§3-3.17b). Fix: write `@unsafe("overlay: <why this address is valid>")` on the declaration (a local may instead sit inside an `unsafe("<reason>") { … }` region)
 --> src/main.lpfree:4:5
  |
4 | var lapic: buffer[uint32, 1024] = 0xFEE0_0000
  |     ^^^^^
  |
```

The warning becomes the error `E_OVERLAY_NOT_UNSAFE` when the package sets `[freestanding] overlays = "error"` (or the compiler runs with `LOUP_OVERLAYS=error`); the Language Reference makes errors the default in a later release, so write the acknowledgment now. On a declaration it is `@unsafe("overlay: …")`; for a local overlay it can also be an enclosing `unsafe("…") { … }` region:

```loup
#~ @mode(freestanding)
#~
@unsafe("overlay: the local APIC register page, identity-mapped at its reset address")
var lapic: buffer[uint32, 1024] = 0xFEE0_0000

func lapicId(): uint32 {
    return lapic[8] >> 24
}

func zeroPage(page: uint): void {
    unsafe("overlay: `page` is a free, mapped 4 KiB frame owned by the caller") {
        var words: buffer[uint64, 512] = page
        for (var i = 0; i < 512; i = i + 1) {
            words[i] = 0u64
        }
    }
}

func copyPage(dst: uint, src: uint): void {
    unsafe("overlay: both frames are mapped, 4 KiB, and do not overlap") {
        var target: buffer[uint64, 512] = dst
        var source: buffer[uint64, 512] = src
        for (var i = 0; i < 512; i = i + 1) {
            target[i] = source[i]
        }
    }
}
```

*Listing 13-2: Acknowledged overlays, and page helpers built from them*

`grep -E '@?unsafe\('` lists every overlay in a code base, with its reason.

The declaration is the *only* way an integer becomes a buffer. Assigning an address to an existing buffer variable is a type error, not a silent re-point:

```loup,does_not_compile
@mode(freestanding)

func clearPage(page: uint64): void {
    var words: buffer[uint64, 512];
    words = page
    for (var i = 0; i < 512; i = i + 1) {
        words[i] = 0u64
    }
}
```

```text
error[E0001]: cannot assign 'uint64' to 'buffer[uint64, 512]'
 --> src/main.lpfree:5:5
  |
5 |     words = page
  |     ^^^^^^^^^^^^
  |
```

Write it as an overlay declaration inside an `unsafe` region, as `zeroPage` in Listing 13-2 does. The same holds for passing an integer for a buffer parameter, storing one into a buffer field, or returning one from a function declared to return a buffer.

A struct field can be an overlay too (`@unsafe("overlay: …") var regs: buffer[uint32, 16] = UART0_BASE;`). Every value of such a struct views the same memory, so the struct is move-only: using it after `let b = a` copies the view, which is the warning `W_OVERLAY_STRUCT_COPY` (an error under `overlays = "error"`).

## Where an allocated buffer lives — `onstack`

An allocated buffer at module level is a static in `.bss`. An allocated buffer **local** is, by default, also a static block — one per function, shared by every call (§3-3.17c). That is fine for a function only ever running once at a time, and wrong for one that can be re-entered: a recursive call, an interrupt that lands while the function runs, or a second CPU would all use the same block.

`onstack` makes the local a real per-invocation frame allocation, or fails to compile and says why:

```loup
#~ @mode(freestanding)
#~
# Walks a 4-level page table; called from the page-fault handler, which can
# nest inside a walk the kernel was already doing.
func walkDepth(root: uint64): int {
    onstack var path: buffer[uint64, 4];
    var depth = 0
    var table = root
    while (depth < 4 and table != 0u64) {
        path[depth] = table
        table = 0u64                 # ... read the next level from `table` ...
        depth = depth + 1
    }
    return depth
}
```

*Listing 13-3: A per-invocation buffer local with `onstack`*

`onstack` is a guarantee, not a hint. If the address of the block could outlive the frame — stored in a global, passed to a function whose parameter the compiler cannot analyse — it is `E_ONSTACK_ESCAPES`, naming the escape. It applies to the allocate form of `buffer` and to struct- or class-typed locals; on a scalar it is `E_ONSTACK_NOT_AGGREGATE` (a scalar is already in the frame). Inside `@interrupt` and `@interruptSafe` functions an aggregate local *must* be `onstack` (`E_INTERRUPT_SAFE_STAGING`, chapter 16). The frame block is zeroed once per call, in the prologue; `@no_zero_locals` suppresses that.

## Raw intrinsics

Below `buffer` sit freestanding intrinsics that work on raw `uint` addresses (§3-3.4.2). They need no import:

| Intrinsic | Does |
|---|---|
| `makeBufferU64(N)`, `makeBufferU32(N)` | reserve a static, zeroed block of `N` 8- or 4-byte elements; evaluate to its address (`N` must be a literal) |
| `bufGetU64(base, i)`, `bufSetU64(base, i, v)` and the `U32`/`U8` forms | read/write element `i` — an **element index, not a byte offset**; gets return `uint64` |
| `bufGetU8Vol(base, i)`, `bufSetU8Vol(base, i, v)` | one volatile byte access each, for byte-wide device registers |
| `addressOf(g)` | the address of a module-level variable `g` |

```loup
#~ @mode(freestanding)
#~
# A 16-word scratch table whose address is handed to firmware.
var scratchTable: uint = 0

func initScratch(): uint {
    scratchTable = makeBufferU64(16)
    bufSetU64(scratchTable, 0, 0x4C4F5550u64)     # element 0: a tag
    bufSetU64(scratchTable, 1, 16u64)             # element 1: the length
    return scratchTable
}

# The second 32-bit word of a record is at element index 1, byte offset 4.
func secondWord(record: uint): uint32 {
    return bufGetU32(record, 1) as uint32
}
```

*Listing 13-4: The raw buffer intrinsics*

Note the `as uint32`: the getters return `uint64`, and widths never mix (chapter 3). Indexes are unchecked, and these calls can reach any address from any function. To make them auditable, a package can require an acknowledgment for each one:

```toml
[freestanding]
rawpoke = "unsafe-only"     # or "warn"; the default is "allow"
```

With that key, a raw access outside an `@unsafe` function or `unsafe("…") { }` region is an error. This function compiles under the default but not under `rawpoke = "unsafe-only"`:

```loup
#~ @mode(freestanding)
#~
# Read the boot loader's handoff word from the scratch page.
func handoffMagic(scratch: uint): uint64 {
    return bufGetU64(scratch, 0)
}
```

*Listing 13-5: A raw read that `rawpoke = "unsafe-only"` rejects*

```text
error[E_RAWPOKE_UNSAFE_ONLY]: E_RAWPOKE_UNSAFE_ONLY: 'bufGetU64' reads/writes an arbitrary address and this package sets [freestanding] rawpoke — move the call into a function annotated @unsafe("reason"), or into an unsafe("reason") { … } region
 --> src/main.lpfree:5:12
  |
5 |     return bufGetU64(scratch, 0)
  |            ^^^^^^^^^^^^^^^^^^^^^
  |
```

Prefer a typed `buffer` where the size is known; keep the raw intrinsics for addresses computed at run time.

## Layout queries

`size_of[T]()` and `align_of[T]()` fold to `uint64` constants for primitives, `@repr(C)`/`@repr(packed)` structs, enums and buffers (chapter 5). Use them to size and place memory:

```loup
#~ @mode(freestanding)
#~
@repr(C)
@align(16)
struct RxDescriptor {
    var bufferAddr: uint64;
    var length: uint16;
    var flags: uint16;
    var status: uint32;
}

const DESC_BYTES = size_of[RxDescriptor]()       # 16
const RING_SLOTS: uint64 = 32u64
const RING_BYTES = DESC_BYTES * RING_SLOTS        # 512
```

*Listing 13-6: Sizing a descriptor ring at compile time*

## `PartialInit[T]` — field-by-field construction

Hardware structures are often filled in one field at a time, from different sources. `PartialInit[T]` tracks, at compile time, which fields of a `@repr(C)` struct have been set, and `complete()` only compiles when all of them have (§3-3.17a):

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

func buildHeader(src: uint16, dst: uint16, payload: uint16): UdpHeader {
    var h = PartialInit[UdpHeader].begin()
    h.set(.srcPort, src)
    h.set(.dstPort, dst)
    h.set(.length, payload + 8u16)
    h.set(.checksum, 0u16)          # 0 = no checksum (IPv4)
    return h.complete()
}
```

*Listing 13-7: Building a header with `PartialInit`*

Forget a field, and `complete()` says which:

```loup,does_not_compile
@mode(freestanding)

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

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

```text
error[E_PARTIAL_INIT_INCOMPLETE]: E_PARTIAL_INIT_INCOMPLETE: `.complete()` called with 1 unset field(s): checksum
  --> src/main.lpfree:16:12
   |
16 |     return h.complete()
   |            ^^^^^^^^^^^^
   |
```

Reading a field before it is set is `E_PARTIAL_INIT_READ`. Tracking follows statement order: a `.set` counts for every statement after it, including one inside a branch, so set every field on the straight-line path before completing.

## Planned primitives

> ⚠ **Planned.** These are specified in the Language Reference but not implemented in Loup 1.25:
>
> - **`MaybeUninit[T]`** (§3-3.17) — a generic slot for not-yet-initialized memory with `uninit()`, `zeroed()`, `rawPtr()` and an `@unsafe` `assumeInit()`. The prelude contains an older, non-generic placeholder class of the same name; `MaybeUninit[T]` is rejected (`type 'MaybeUninit' expects 0 type argument(s), got 1`). Use an allocated buffer (zeroed) or `PartialInit[T]` instead.
> - **`reinterpret[T, U]` and `reinterpretChecked[T, U]`** (§3-3.18) — bit-pattern reinterpretation with compile-time size and alignment checks. The 1.25 checker parses `reinterpret[T, U](x)` but treats the result as having the *source* type and checks nothing, so do not use it. `reinterpretChecked` and its `AlignmentError` are not declared.
> - **`native.mem`** (`copy`, `zero`, `compare`, `alloc`, `free`, `tryAlloc`) and **`native.ffi`** (§3-3.20) — these modules do not exist for freestanding code; importing them is an error (chapter 20). Write the loops with overlays as in Listing 13-2, and call your `@allocator` directly (chapter 7).

The compiler itself still needs `memcpy`/`memset`/`memcmp` for struct copies and zero-fills; the toolchain links its own freestanding implementation of those automatically, so you never write or import them.

## Putting it together — a receive descriptor ring

A network card reads its receive ring from memory: a contiguous, 16-byte-aligned array of descriptors whose base address the driver writes to a device register. A module-level buffer of `@repr(C)` structs is exactly that:

```loup
#~ @mode(freestanding)
#~
@repr(C)
@align(16)
struct RxDescriptor {
    var bufferAddr: uint64;
    var length: uint16;
    var flags: uint16;
    var status: uint32;
}

const RING_SLOTS = 32
const DESC_DONE: uint32 = 0x1u32

@align(16)
var rxRing: buffer[RxDescriptor, RING_SLOTS];
var rxHead: int = 0

# The value the driver writes to the card's ring-base register.
func ringBase(): uint {
    return addressOf(rxRing)
}

# Give slot `i` a fresh 2 KiB packet buffer.
func armSlot(i: int, packetBuffer: uint64): void {
    rxRing[i].bufferAddr = packetBuffer
    rxRing[i].length = 2048u16
    rxRing[i].status = 0u32
}

# Returns the length of the next completed packet, or 0 if none is ready.
func pollReceive(): uint16 {
    if ((rxRing[rxHead].status & DESC_DONE) == 0u32) {
        return 0u16
    }
    let length = rxRing[rxHead].length
    rxHead = (rxHead + 1) % RING_SLOTS
    return length
}
```

*Listing 13-8: A receive ring as an aligned module buffer*

Two cautions before this goes near real hardware. The device writes `status` behind the compiler's back, so the poll must be a volatile read — use a `Volatile` view or a `volatile borrow` of the descriptor (chapter 15). And on weakly ordered CPUs a barrier is needed between filling a descriptor and telling the device about it (chapter 19).

## Summary

- `buffer[T, N]` is a fixed block of `N` elements at `size_of[T]()` stride; constant indexes are checked (`E_BUFFER_INDEX_RANGE`), run-time indexes only with `[freestanding] bounds = "trap"`; buffer parameters must match `N` exactly.
- An integer initializer makes an overlay; acknowledge it with `@unsafe("overlay: …")` or an `unsafe("…") { }` region (`W_OVERLAY_NOT_UNSAFE`, an error under `overlays = "error"`). Only the declaration turns an address into a buffer.
- Allocated buffer locals are shared static blocks unless declared `onstack`.
- `makeBufferU64`, `bufGet*`/`bufSet*` (element indexes, `uint64` results) and `addressOf` work on raw addresses; `rawpoke = "unsafe-only"` makes each use auditable.
- `PartialInit[T]` checks field-by-field construction at compile time.
- `MaybeUninit[T]`, `reinterpret`, `reinterpretChecked`, `native.mem` and `native.ffi` are planned, not available.

Next: inline assembly.
