# Chapter 5: Structs and Layout Control

Most hardware interfaces have a layout someone else chose. The CPU vendor decided what an interrupt frame and a task state segment look like; the network standard fixed the bytes of a UDP header; the SoC datasheet says which register sits at offset `0x14`. In freestanding code, many of your `struct` declarations exist to match those layouts exactly.

Loup gives you three layout annotations — `@repr(C)`, `@repr(packed)` and `@repr(transparent)` — plus `@align(n)`, and two compile-time queries, `size_of[T]()` and `align_of[T]()`. This chapter covers each, with the mistakes that come from C and Rust habits.

## Struct basics

A struct is a value type: assigning or passing one copies it. Fields declared with `var` are mutable; a field written without a keyword is immutable after construction. A struct is constructed with named arguments, and methods are declared inside the struct body.

```loup
@mode(freestanding)

# An x86_64 IDT gate, before it is encoded into its 16-byte hardware form.
struct Gate {
    selector: uint16;            # fixed when the gate is built
    var offset: uint64;          # retargeted when a handler is installed
    var present: bool;

    func isInstalled(): bool {
        return this.present and this.offset != 0u64
    }
}

func installHandler(handler: uint64): Gate {
    var g = Gate(selector = 0x08u16, offset = 0u64, present = false)
    g.offset = handler
    g.present = true
    return g
}
```

*Listing 5-1: A struct with immutable and mutable fields and a method*

Trying to change the immutable field after construction is an error:

```loup,does_not_compile
@mode(freestanding)

struct Gate {
    selector: uint16;
    var offset: uint64;
}

func retarget(handler: uint64): Gate {
    var g = Gate(selector = 0x08u16, offset = 0u64)
    g.offset = handler
    g.selector = 0x10u16
    return g
}
```

```text
error[E0001]: cannot assign to immutable field 'selector' of struct 'Gate'
  --> src/main.lpfree:11:5
   |
11 |     g.selector = 0x10u16
   |     ^^^^^^^^^^^^^^^^^^^^
   |
```

Either build a new `Gate` with the other selector, or declare the field `var selector: uint16;` if changing it is part of the design.

Two Rust habits do not carry over: there is no separate `impl` block (methods go inside the struct), and there is no `Name { field: value }` literal (construction is a call with named arguments, `Name(field = value)`). Writing `impl Pte { … }` produces `error[E0003]: expected `;`` at the type name, followed by a cascade.

## Default layout, and why `size_of` refuses it

Without a `@repr` annotation a struct's layout is the compiler's business. That is fine for a struct that never leaves Loup code, but it means the struct has no size you can rely on, and `size_of` says so:

```loup,does_not_compile
@mode(freestanding)

struct Descriptor {
    var base: uint32;
    var limit: uint16;
}

func descriptorBytes(): uint64 {
    return size_of[Descriptor]()
}
```

```text
error[E_SIZE_OF_UNSIZED]: E_SIZE_OF_UNSIZED: `size_of[Descriptor]()` — `Descriptor` has no statically-known finite size, so there is no single number to answer with.
 --> src/main.lpfree:9:12
  |
9 |     return size_of[Descriptor]()
  |            ^^^^^^^^^^^^^^^^^^^^^
  |
```

Anything that crosses a boundary — a register block, a network header, a DMA descriptor, a structure the CPU reads, a C function's argument — needs an explicit `@repr`. With `@repr(C)` the same query answers `8` (a `uint32` at offset 0, a `uint16` at offset 4, rounded up to 4-byte alignment).

## `@repr(C)` — declaration order, natural alignment

`@repr(C)` lays fields out in declaration order, each at the next multiple of its own alignment, and rounds the struct up to its widest alignment (Language Reference §3-3.10 and the layout rules in §3-3.4.1). It is the layout a C compiler would choose.

```loup
#~ @mode(freestanding)
#~
@repr(C)
struct ElfHeader {
    var ident: buffer[uint8, 16];   # magic, class, data, version, ABI
    var fileType: uint16;
    var machine: uint16;
    var version: uint32;
    var entry: uint64;
    var phoff: uint64;
    var shoff: uint64;
    var flags: uint32;
    var ehsize: uint16;
    var phentsize: uint16;
    var phnum: uint16;
    var shentsize: uint16;
    var shnum: uint16;
    var shstrndx: uint16;
}

const ELF_HEADER_BYTES = size_of[ElfHeader]()     # 64
```

*Listing 5-2: The ELF64 file header as a `@repr(C)` struct*

A fixed-length array field is a `buffer[T, N]` — `N` elements of `T`, laid out inline (chapter 13 covers buffers in full). The C-style `tuple[uint8; 16]` that older Loup material used is not valid syntax:

```loup,does_not_compile
@mode(freestanding)

@repr(C)
struct ElfIdent {
    var magic: tuple[uint8; 4];
    var elfClass: uint8;
}
```

```text
error[E0001]: expected RBracket, found Semicolon
 --> src/main.lpfree:5:27
  |
5 |     var magic: tuple[uint8; 4];
  |                           ^
  |

error[E0001]: unexpected IntLiteral in struct body
 --> src/main.lpfree:5:29
  |
5 |     var magic: tuple[uint8; 4];
  |                             ^
  |
```

Write `var magic: buffer[uint8, 4];`.

### `extern native` structs must be `@repr(C)`

A struct passed to or returned from an `extern native` function crosses into code compiled by a C compiler, so its layout must be C's:

```loup,does_not_compile
@mode(freestanding)

struct Timespec {
    var seconds: int64;
    var nanos: int64;
}

extern native "libfwclock" func fw_clock_read(ts: Timespec): int32;
```

```text
error[E_EXTERN_STRUCT_NOT_REPR_C]: E_EXTERN_STRUCT_NOT_REPR_C: struct `Timespec` crosses the `extern native` boundary of `fw_clock_read` in freestanding mode without `@repr(C)` — a struct passed to or returned from an `extern native` function must carry `@repr(C)` so its layout matches the C ABI (§2-3.4)
 --> src/main.lpfree:8:1
  |
8 | extern native "libfwclock" func fw_clock_read(ts: Timespec): int32;
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
```

Adding `@repr(C)` above `struct Timespec` fixes it. Each `extern native "library"` declaration names one C function the image links against.

## Checking a layout at compile time

Getting a hardware structure's size wrong is a quiet bug: the code compiles, and the CPU reads garbage. You can make the compiler check the size. A `const` is evaluated during compilation, and a `const func` that reaches `unreachable` stops that evaluation with an error:

```loup,does_not_compile
@mode(freestanding)

# Fails compilation when a hardware-defined layout has the wrong size.
const func expectSize(actual: uint64, expected: uint64): uint64 {
    if (actual != expected) {
        unreachable
    }
    return actual
}

# x86_64 Task State Segment (Intel SDM Vol. 3, "64-bit TSS format"): 104 bytes.
@repr(C)
struct TaskStateSegment {
    var reserved0: uint32;
    var rsp0: uint64;
    var rsp1: uint64;
    var rsp2: uint64;
    var reserved1: uint64;
    var ist: buffer[uint64, 7];
    var reserved2: uint64;
    var reserved3: uint16;
    var ioMapBase: uint16;
}

const TSS_BYTES = expectSize(size_of[TaskStateSegment](), 104u64)
```

```text
error[E_CONST_NOT_EVALUABLE]: `const TSS_BYTES`: the initialiser is a const-expression in form, but evaluating it reached this expression, which the const evaluator does not support — a `const` is decided at compile time or not at all; use `let` for a runtime value (E_CONST_NOT_EVALUABLE)
  --> src/main.lpfree:25:19
   |
25 | const TSS_BYTES = expectSize(size_of[TaskStateSegment](), 104u64)
   |                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
```

The message is generic — the evaluator "reached" the `unreachable` — but it points at the right line, and the check caught a real bug. Under `@repr(C)`, `rsp0` is aligned to offset 8, so the struct is 112 bytes. The hardware format puts `rsp0` at offset 4: the TSS is a packed structure. The fix is the layout annotation, and the same check now passes:

```loup
#~ @mode(freestanding)
#~
#~ const func expectSize(actual: uint64, expected: uint64): uint64 {
#~     if (actual != expected) {
#~         unreachable
#~     }
#~     return actual
#~ }
#~
@repr(packed)
struct TaskStateSegment {
    var reserved0: uint32;
    var rsp0: uint64;
    var rsp1: uint64;
    var rsp2: uint64;
    var reserved1: uint64;
    var ist: buffer[uint64, 7];
    var reserved2: uint64;
    var reserved3: uint16;
    var ioMapBase: uint16;
}

const TSS_BYTES = expectSize(size_of[TaskStateSegment](), 104u64)
```

*Listing 5-3: A compile-time size check on the x86_64 TSS*

## `@repr(packed)` — no padding

`@repr(packed)` keeps declaration order but drops all padding, inside the struct and at its end. Fields may then sit at addresses that are not aligned for their type; the compiler emits unaligned-safe loads and stores for them, which is defined behaviour in Loup (§3-3.10). Use it for wire formats and for hardware structures that are specified as packed, like the TSS above.

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

@repr(packed)
struct EthernetHeader {
    var dst: buffer[uint8, 6];
    var src: buffer[uint8, 6];
    var etherType: uint16;
}

const UDP_HEADER_BYTES = size_of[UdpHeader]()           # 8
const ETHERNET_HEADER_BYTES = size_of[EthernetHeader]() # 14, not rounded up to 16

func invertChecksum(edits h: UdpHeader): void {
    h.checksum = h.checksum ^ 0xFFFFu16
}
```

*Listing 5-4: Packed wire-format headers*

Two cautions. Network headers are big-endian on the wire; a packed struct fixes the *positions* of the bytes, not their order, so convert multi-byte fields when you read or write them (`@interchange` in the Language Reference §4-2 does that for you on hosted targets). And an unaligned field is still unaligned when you hand its address to hardware or to `asm`: copy it into an aligned local first.

## `@repr(transparent)` — zero-cost newtypes

`@repr(transparent)` is for a struct with exactly one field that must have the same representation and calling convention as that field. It turns a raw integer into a distinct type at no cost — a physical address cannot be passed where a virtual address is expected:

```loup
#~ @mode(freestanding)
#~
@repr(transparent)
struct PhysAddr {
    var raw: uint64;
}

@repr(transparent)
struct VirtAddr {
    var raw: uint64;
}

# The kernel maps all physical memory at this offset.
let PHYS_MAP_BASE: uint64 = 0xFFFF_8000_0000_0000u64

func physToVirt(p: PhysAddr): VirtAddr {
    return VirtAddr(raw = p.raw + PHYS_MAP_BASE)
}
```

*Listing 5-5: Newtypes for physical and virtual addresses*

`PHYS_MAP_BASE` is a module `let`, not a `const`: in 1.25 the compile-time evaluator reads integer literals as signed 64-bit values, so a `const` initialized with a `uint64` literal above `0x7FFF_FFFF_FFFF_FFFF` is rejected as an overflow (`E_CONST_NOT_EVALUABLE`). A `let` holds the same value.

The Language Reference requires exactly one non-zero-sized field (`E_REPR_TRANSPARENT_MULTI_FIELD`) and gives the struct the size and ABI of that field. The 1.25 checker does not report a second field yet, so keep transparent structs to one field yourself. It also does not answer `size_of` for a transparent struct yet (it reports `E_SIZE_OF_UNSIZED`); query the inner type instead.

## `@align(n)` — minimum alignment

`@align(n)` raises the alignment of a struct type, or of a module-level variable, to `n` bytes; `n` must be a power of two and at least the natural alignment.

```loup
#~ @mode(freestanding)
#~
# An x86_64 page table: 512 entries, and the hardware requires 4 KiB alignment.
@align(4096)
@repr(C)
struct PageTable {
    var entries: buffer[uint64, 512];
}

const PAGE_TABLE_BYTES = size_of[PageTable]()     # 4096
const PAGE_TABLE_ALIGN = align_of[PageTable]()    # 4096

# One counter per CPU, each on its own 64-byte cache line (no false sharing).
@align(64)
@repr(C)
struct PerCpuCounter {
    var count: uint64;
}

var cpuCounters: buffer[PerCpuCounter, 4];

func bump(cpu: int): void {
    cpuCounters[cpu].count = cpuCounters[cpu].count + 1u64
}
```

*Listing 5-6: Page-aligned and cache-line-aligned types*

The annotation applies to every instance of the type: module globals, buffer elements, locals. The rules that `n` be a power of two (`E_ALIGN_NOT_POWER_OF_TWO`) and not smaller than the natural alignment (`E_ALIGN_TOO_SMALL`) are specified but not yet checked, so an `@align(3)` slips through `loup check` today.

## `size_of[T]()` and `align_of[T]()`

Both are compile-time expressions that fold to a `uint64` constant against the build target; no import is needed and they cost nothing at run time (§3-3.4.1). The type must have a known layout — a primitive, a `@repr` struct, an enum, a tuple or a buffer. A class does not:

```loup,does_not_compile
@mode(freestanding)

class Task {
    pub var id: uint32 = 0;
    pub var stackTop: uint64 = 0;
}

func taskBytes(): uint64 {
    return size_of[Task]()
}
```

```text
error[E_SIZE_OF_CLASS]: E_SIZE_OF_CLASS: `size_of[Task]()` — `Task` is a managed class, so a binding of that type holds a REFERENCE to an ARC-headered heap object. Ask for the layout of a `@repr(C)` struct, a primitive, an enum or a tuple.
 --> src/main.lpfree:9:12
  |
9 |     return size_of[Task]()
  |            ^^^^^^^^^^^^^^^
  |
```

A variable of class type is a reference; if you need to know how big the object behind it is, describe the data as a `@repr(C)` struct.

## A page-table entry type

Putting the pieces together — a transparent entry type with its flag constants and methods, and a page-aligned table of them:

```loup
#~ @mode(freestanding)
#~
const PTE_PRESENT: uint64 = 1u64 << 0
const PTE_WRITABLE: uint64 = 1u64 << 1
const PTE_USER: uint64 = 1u64 << 2
const PTE_NO_EXECUTE: uint64 = 1u64 << 63
const PTE_FRAME_MASK: uint64 = 0x000F_FFFF_FFFF_F000u64

@repr(transparent)
struct Pte {
    var raw: uint64;

    pub static func empty(): Pte {
        return Pte(raw = 0u64)
    }

    pub static func map(frame: uint64, flags: uint64): Pte {
        return Pte(raw = (frame & PTE_FRAME_MASK) | flags | PTE_PRESENT)
    }

    pub func isPresent(): bool {
        return (this.raw & PTE_PRESENT) != 0u64
    }

    pub func frame(): uint64 {
        return this.raw & PTE_FRAME_MASK
    }
}

@align(4096)
@repr(C)
struct PageTable {
    var entries: buffer[Pte, 512];
}

func mapIdentity(edits table: PageTable, index: int): void {
    let frame = (index as uint64) * 4096u64
    table.entries[index] = Pte.map(frame, PTE_WRITABLE | PTE_NO_EXECUTE)
}
```

*Listing 5-7: A page-table entry newtype with methods*

`Pte.map(...)` is a static method call; `table.entries[index]` writes one 8-byte element of the table in place. Chapter 23 builds a safe mapping API on top of this.

## Field defaults

A field may declare a default. A struct variable declared without an initializer, `var p: T;`, gets every field's default — or zero when the field has none — on every path, so no stale stack bytes leak through:

```loup
#~ @mode(freestanding)
#~
@repr(C)
struct RxDescriptor {
    var bufferAddr: uint64 = 0;
    var length: uint16 = 0;
    var status: uint8 = 0;
    var errors: uint8 = 0;
}

func freshDescriptor(addr: uint64): RxDescriptor {
    var d: RxDescriptor;          # all fields start at their defaults
    d.bufferAddr = addr
    return d
}
```

*Listing 5-8: A default-initialized descriptor*

## Field privacy across modules

A field declared without `pub` is private to the module that declares the struct. Code in that module reads and writes it; code in any other module that writes it is rejected. Here `caps.lpfree` declares a capability type whose identifier field is private:

```loup
#~ @mode(freestanding)
#~
pub struct Cap {
    var idRaw: uint64 = 0;           # private to this module
    pub var rights: uint64 = 0;
}

pub func mint(id: uint64): Cap {
    return Cap(idRaw = id, rights = 1u64)
}

pub func idOf(c: Cap): uint64 {
    return c.idRaw
}
```

*Listing 5-9: `src/caps.lpfree` — a struct with a private field*

and another module tries to change a capability it was given:

```loup,ignore
@mode(freestanding)

from caps import (Cap, mint, idOf);

func upgrade(): uint64 {
    var c = mint(7u64)
    c.idRaw = 0u64          # try to turn it into capability 0
    return idOf(c)
}
```

```text
error[E_FIELD_PRIVATE]: member 'idRaw' of struct 'Cap' is private
 --> src/main.lpfree:7:5
  |
7 |     c.idRaw = 0u64          # try to turn it into capability 0
  |     ^^^^^^^
  |
```

(This listing imports a sibling module, so it is shown with the output `loup check` printed for the two-file project rather than checked on its own.)

There is one gap to know about: in 1.25, *constructing* the struct from another module with named arguments — `Cap(idRaw = 0u64, rights = 7u64)` — is not rejected. Until it is, do not rely on private fields alone to make a type unforgeable; keep the type's constructor-like functions as the only documented way to make one, and review imports of the type.

## Summary

- Structs are values; `var` fields are mutable, keyword-less fields are fixed at construction. Methods live inside the struct; construct with `Name(field = value)`.
- Without `@repr` the layout is unspecified and `size_of` refuses the type (`E_SIZE_OF_UNSIZED`).
- `@repr(C)`: declaration order with natural alignment — required for `extern native` structs (`E_EXTERN_STRUCT_NOT_REPR_C`).
- `@repr(packed)`: no padding; fields may be unaligned. `@repr(transparent)`: one-field newtypes with the field's ABI.
- Fixed-length array fields are `buffer[T, N]`, not `tuple[T; N]`.
- `@align(n)` raises alignment for types and module variables.
- `size_of[T]()`/`align_of[T]()` fold to constants; a `const func` that reaches `unreachable` turns a wrong size into a compile error.
- Non-`pub` fields cannot be written from another module (`E_FIELD_PRIVATE`), but cross-module construction is not yet blocked.

Next: enums and pattern matching.
