# Chapter 7: Classes and Allocators

In the hosted modes a class instance lives on a heap managed by ARC. A freestanding image has neither a heap nor ARC, so classes need more care. This chapter covers:

- declaring and constructing classes in freestanding, and where the objects actually live in Loup 1.25;
- why `unique class` is the class kind to use;
- registering an `@allocator`/`@deallocator` pair, and the signature mistakes the compiler catches;
- `del`, `finalize()` and `consume`;
- patterns that avoid dynamic allocation altogether.

## Declaring and constructing a class

Class syntax is the same as everywhere else: fields are declared `let` or `var`, the constructor is `new(...)`, and methods can carry receiver verbs (chapter 10).

```loup
@mode(freestanding)

unique class Task {
    pub var id: uint32;
    pub var stackTop: uint64;
    pub var runnable: bool = false;

    new(id: uint32, stackTop: uint64) {
        this.id = id
        this.stackTop = stackTop
    }

    reads func describe(): uint32 {
        return this.id
    }

    modifier func wake(): void {
        this.runnable = true
    }
}

func startIdleTask(): uint32 {
    var idle = Task(0u32, 0x9_0000u64)
    idle.wake()
    return idle.describe()
}
```

*Listing 7-1: A `unique class` with a constructor and methods*

`Task(…)` and `new Task(…)` are the same construction. A field without `pub` is private to the class.

## Where a class object lives in 1.25

The Language Reference says that in freestanding a class can only be constructed once the program registers an `@allocator`, that construction allocates through it, and that constructing a class without one is `E_FREESTANDING_CLASS_NO_ALLOC` (§3-3.3, §3-3.16).

> ⚠ **Planned.** The 1.25 compilers do not implement that integration. `loup check` accepts class construction with or without an allocator, and the freestanding back ends never call the `@allocator` function: they keep it in the image and check its signature, but place each object the way they place a struct value — in the constructing function's stack frame when the analysis can prove that is safe, otherwise in a static block reserved for that construction site (Language Reference §3-3.17c, §3-3.24.1).

The static-block case is the one to watch. A static block belongs to the *site*, so two executions of the same function — a recursive call, an interrupt that lands while the function runs, a second CPU — share one object. `onstack` turns that into a guarantee of per-invocation storage, or a compile error naming why it cannot be honoured:

```loup
#~ @mode(freestanding)
#~
unique class Port {
    pub var base: uint16;
    new(base: uint16) { this.base = base; }
}

# Called from the main loop and from the serial interrupt handler: each
# activation needs its own Port object.
func statusPort(base: uint16): uint16 {
    onstack var port: Port = Port(base)
    return port.base + 5u16
}
```

*Listing 7-2: `onstack` gives a class local per-invocation storage*

`onstack` needs a declared struct or class type on the binding, as here; chapter 13 covers its rules.

## `unique class` versus `class`

A plain `class` value is a reference. In the hosted modes ARC counts those references; in freestanding nothing does, and the checker lets you copy one freely:

```loup
#~ @mode(freestanding)
#~
class Counter {
    pub var hits: uint32 = 0;
}

func twoNames(): uint32 {
    let a = Counter()
    let b = a              # a second reference to the same object
    return a.hits + b.hits
}
```

*Listing 7-3: A plain class reference can be aliased*

With no reference count, nobody knows when the last reference is gone. A `unique class` has exactly one owner at a time: assigning it moves it, and using the old binding afterwards is `E_USE_AFTER_MOVE` (chapter 12). Single ownership is what lets `del` and `takes` say precisely when an object's life ends, so `unique class` is the kind to use for anything that owns a resource — a device, a DMA region, a lease on a pool slot.

## Registering `@allocator` and `@deallocator`

An allocator is a pair of module-level functions with fixed signatures (§3-3.16):

- `@allocator func name(size: uint64, align: uint64): uint?` — `Some(address)` on success, `None` when out of memory;
- `@deallocator func name(ptr: uint, size: uint64, align: uint64): void` — receives the size and alignment of the original request.

Here is a bump allocator over a 64 KiB module buffer:

```loup
#~ @mode(freestanding)
#~
# 64 KiB of heap in .bss, as 8-byte words.
var heapWords: buffer[uint64, 8192];
var heapNext: uint64 = 0
const HEAP_BYTES: uint64 = 65536u64

@allocator
func bumpAlloc(size: uint64, align: uint64): uint? {
    let base = addressOf(heapWords) as uint64
    let mask = align - 1u64
    let start = (heapNext + mask) & bnot mask     # round up to `align`
    if (start + size > HEAP_BYTES) {
        return None                               # out of memory
    }
    heapNext = start + size
    return Some((base + start) as uint)
}

@deallocator
func bumpFree(ptr: uint, size: uint64, align: uint64): void {
    # A bump allocator never reuses memory; a free list would go here.
    return
}
```

*Listing 7-4: A bump allocator registered as the image's allocator*

`addressOf(g)` is a freestanding intrinsic that yields the address of a module-level variable (§3-3.4.2). The allocator is not thread-safe; chapter 19 shows how to make the bump step atomic.

The return type is the most common mistake. A C allocator returns 0 for "out of memory", and a C habit writes this:

```loup,does_not_compile
@mode(freestanding)

var heapWords: buffer[uint64, 8192];
var heapNext: uint64 = 0

# C habit: return 0 for "out of memory".
@allocator
func bumpAlloc(size: uint64, align: uint64): uint {
    if (heapNext + size > 65536u64) {
        return 0
    }
    let addr = (addressOf(heapWords) as uint64) + heapNext
    heapNext = heapNext + size
    return addr as uint
}

@deallocator
func bumpFree(ptr: uint, size: uint64, align: uint64): void {
    return
}
```

```text
src/main.lpfree:7:1: error: E_ALLOCATOR_SIG_V16: `@allocator` function must return `uint?` (nullable unsigned integer) in freestanding mode
```

(This is the one-line form `loup check --format short` prints.) Address 0 can be a valid physical address on bare metal, so Loup makes failure a separate value: return `uint?`, with `None` for failure, as in Listing 7-4.

The deallocator's signature is checked the same way. A deallocator that takes only the pointer — the C `free(void *)` shape — is rejected:

```loup,does_not_compile
@mode(freestanding)

@allocator
func poolAlloc(size: uint64, align: uint64): uint? {
    return None
}

@deallocator
func poolFree(ptr: uint): void {
    return
}
```

```text
error[E_DEALLOCATOR_SIG]: E_DEALLOCATOR_SIG: `@deallocator` `poolFree` must be `(ptr: uint, size: uint64, align: uint64): void`; it is `(uint): void` (§3-3.16)
 --> src/main.lpfree:8:1
  |
8 | @deallocator
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
```

Passing the size back means a size-class allocator never has to store a header in front of each block. The Language Reference adds three more rules — at most one of each per program (`E_ALLOCATOR_DUPLICATE`, `E_DEALLOCATOR_DUPLICATE`), the two must come as a pair (`E_ALLOCATOR_UNPAIRED`), and both are freestanding-only (`E_ALLOCATOR_NOT_FREESTANDING`). The 1.25 checker does not report those yet; keep them by hand.

### Calling the allocator yourself

An `@allocator` function is also an ordinary function. Until class construction routes through it, calling it directly is how you get dynamic memory — for a packet buffer, a page-table page, a per-task stack:

```loup
#~ @mode(freestanding)
#~
#~ var heapWords: buffer[uint64, 8192];
#~ var heapNext: uint64 = 0
#~ const HEAP_BYTES: uint64 = 65536u64
#~
#~ @allocator
#~ func bumpAlloc(size: uint64, align: uint64): uint? {
#~     let base = addressOf(heapWords) as uint64
#~     let mask = align - 1u64
#~     let start = (heapNext + mask) & bnot mask
#~     if (start + size > HEAP_BYTES) {
#~         return None
#~     }
#~     heapNext = start + size
#~     return Some((base + start) as uint)
#~ }
#~
#~ @deallocator
#~ func bumpFree(ptr: uint, size: uint64, align: uint64): void {
#~     return
#~ }
#~
enum AllocError { OutOfMemory }

# A 16 KiB kernel stack for a new task; returns the address of its top.
func allocStack(): Result[uint, AllocError] {
    match bumpAlloc(16384u64, 16u64) {
        Some(base) => { return Ok(base + 16384) }
        None => { return Err(AllocError.OutOfMemory) }
    }
}
```

*Listing 7-5: Turning allocation failure into an error value*

The Language Reference's `native.mem.alloc`/`free`/`tryAlloc` wrappers over the registered allocator (§3-3.20) are not available in freestanding in 1.25 (chapter 20 lists what is).

## `del` and `finalize()`

`del x` ends a binding's life early and runs the class's `finalize()` method, if it has one (Language Reference §1-6.11). After `del`, the binding is uninitialized, and the compiler rejects any later use:

```loup,does_not_compile
@mode(freestanding)

unique class Lease {
    pub var slot: uint32;
    new(slot: uint32) { this.slot = slot; }
    func finalize(): void {
        # return the slot to the pool here
    }
}

func releaseEarly(): uint32 {
    let lease = Lease(3u32)
    del lease
    return lease.slot
}
```

```text
error[E_USE_AFTER_DEL]: E_USE_AFTER_DEL: use of binding 'lease' after `del` — the binding is uninitialized once it has been passed to `del` (§1-6.11)
  --> src/main.lpfree:14:12
   |
14 |     return lease.slot
   |            ^^^^^
   |
```

Read what you need first, then `del`:

```loup
#~ @mode(freestanding)
#~
#~ unique class Lease {
#~     pub var slot: uint32;
#~     new(slot: uint32) { this.slot = slot; }
#~     func finalize(): void {
#~     }
#~ }
#~
func releaseEarly(): uint32 {
    let lease = Lease(3u32)
    let slot = lease.slot
    del lease                   # finalize() runs here
    return slot
}
```

*Listing 7-6: Reading a field before `del`*

The Language Reference also has `del` hand the object's memory to the `@deallocator`; like allocation, that half waits for the planned allocator integration.

## `consume` does not exist here

In `host.native`, `consume x` turns a `unique` value into an ordinary ARC reference. There is no ARC in freestanding, so the downgrade is meaningless:

```loup,does_not_compile
@mode(freestanding)

unique class DmaRegion {
    pub var base: uint64;
    new(base: uint64) { this.base = base; }
}

func publish(): uint64 {
    unique let region = DmaRegion(0x8000_0000u64)
    let counted = consume region
    return counted.base
}
```

```text
error[E_CONSUME_NOT_AVAILABLE]: E_CONSUME_NOT_AVAILABLE: `consume` is not available in @mode(freestanding) mode — the unique-to-ARC downgrade requires an ARC runtime, which exists only in `@mode(host.native)` or `@mode(host.dual)`
  --> src/main.lpfree:10:19
   |
10 |     let counted = consume region
   |                   ^^^^^^^^^^^^^^
   |

error[E_UNIQUE_CLASS_CONSUME]: E_UNIQUE_CLASS_CONSUME: `consume` on unique class binding 'DmaRegion' is not allowed — unique class instances cannot be downgraded to ARC
  --> src/main.lpfree:10:19
   |
10 |     let counted = consume region
   |                   ^^^^^^^^^^^^^^
   |
```

Keep the single owner and lend the object instead: pass it to a `reads` or `borrow` parameter, or transfer it with `takes` when the receiver should own it (chapters 10–12).

## Designs that need no allocator

Many freestanding programs never allocate after boot. A boot shim runs once; a microcontroller has a fixed set of peripherals; a kernel subsystem can size its tables at build time. The patterns:

**Module-level tables.** A `buffer` at module scope is zero-initialized storage in `.bss`, with no allocator involved:

```loup
#~ @mode(freestanding)
#~
@repr(C)
struct TaskSlot {
    var used: bool;
    var stackTop: uint64;
    var entry: uint64;
}

# A handle is just an index into the table.
@repr(transparent)
struct TaskHandle {
    var index: uint16;
}

var tasks: buffer[TaskSlot, 64];

func allocTask(entry: uint64): Option[TaskHandle] {
    var i = 0
    while (i < 64) {
        if (not tasks[i].used) {
            tasks[i].used = true
            tasks[i].entry = entry
            return Some(TaskHandle(index = i as uint16))
        }
        i = i + 1
    }
    return None                       # table full
}

func freeTask(h: TaskHandle): void {
    tasks[h.index as int].used = false
}
```

*Listing 7-7: A fixed task table handing out index handles*

**Frame storage.** Struct locals and `onstack` buffers live in the function's frame and disappear with it (chapter 13).

**Values instead of objects.** Most freestanding data — descriptors, register snapshots, configuration — is better modelled as a `@repr(C)` struct than as a class: it has a known layout, `size_of` answers for it, and copying it is explicit.

## Summary

- Classes are declared and constructed as in other modes; `unique class` gives single ownership, which is what freestanding code needs. A plain class reference can be aliased and nothing counts the references.
- The Language Reference routes class construction through the registered `@allocator`; in 1.25 that is not implemented — objects are staged in the frame or in a per-site static block. Declare re-entrant locals `onstack`.
- `@allocator` is `(size: uint64, align: uint64): uint?`; a plain `uint` return is `E_ALLOCATOR_SIG_V16`. `@deallocator` is `(ptr: uint, size: uint64, align: uint64): void` (`E_DEALLOCATOR_SIG`). Call the allocator directly for dynamic memory today.
- `del` runs `finalize()`; using the binding afterwards is `E_USE_AFTER_DEL`. `consume` is unavailable (`E_CONSUME_NOT_AVAILABLE`, `E_UNIQUE_CLASS_CONSUME`).
- Fixed tables with index handles, frame storage and plain structs avoid dynamic allocation entirely.

Next: interfaces and generics.
