Memory

Lumina's memory model aims for "Rust-like performance where you need it, with no borrow checker to fight elsewhere."

The model in one paragraph

You never write &, &mut, lifetimes or free. The compiler's escape analysis puts values that do not escape their function on the stack. Values that do escape (returned, stored in a long-lived structure, shared between owners) live on a generational, concurrently marking GC heap with per-thread nurseries. On top of that sit opt-in, compiler-checked annotations for code that must not allocate at all, plus mut-as-reference semantics (see Mutability) that make in-place mutation explicit.

So the GC is the default safety net, not a requirement. Hot code can be proven allocation-free, and the compiler tells you where the net catches something.

The compiler shows you GC fallbacks

struct Node {
    public value: Integer
    public children: Array<Node>
}

function makeTree(depth: Integer): Node {
    mut kids: Array<Node> = []
    if depth > 0 {
        kids.push(makeTree(depth - 1))
        kids.push(makeTree(depth - 1))
    }
    return Node { value: depth, children: kids }
}
$ lumina build tree.lumina -o tree
warning[GC-FALLBACK]: value of type Node in function makeTree falls back to the GC heap (reason: returned) ...
Build successful

A tree that is returned has to outlive the function, so it goes to the GC heap, and lumina build says so by default. Pass --no-warn-gc-fallback to silence it, or --no-gc to make any GC allocation in the program a build error (E2052). --no-gc is reachability-based: it checks every function reachable from main with the same rules as @Memory("noGc"), there is no annotation to add, and @Memory.allowGc is no way out. Code that is never called does not count. String literals are fine; returning or storing a struct, building strings or collections is not, so avoid returning or storing values and use an owned MutableBytes buffer instead. A program that passes builds without the collector, which makes the binary smaller. Each error shows the call chain from main down to the allocation and suggests a fix, such as an owned MutableBytes buffer in place of a string or collection. If a program passes that check but still needs the collector, for example because a type has a Drop finalizer, the build fails with E2063 and names the reason.

Allocation-free functions: @Memory.noAlloc

import from "std/memory"

@Memory.noAlloc
function dot(ax: Float, ay: Float, az: Float, bx: Float, by: Float, bz: Float): Float = ax * bx + ay * by + az * bz

This compiles, because nothing in it allocates. Now add a hidden allocation:

@Memory.noAlloc
function label(name: String, id: Integer): String = name + "#" + `{id}`
error[E2051]: @Memory.noAlloc function 'label' performs a heap alloc — struct literals,
array literals, and String concatenation are not permitted; use stack-only types or
remove @Memory.noAlloc

The check is transitive: calling an allocating function from a noAlloc one is also an error. Its sibling @Memory.deterministic allows explicitly marked @Memory.allowGc helpers. Function-level @Memory.stack forces stack placement and rejects escapes (E2053).

These are the tools for audio callbacks, parsers and request hot paths: you state the property once, and the compiler keeps it true as the code changes.

Frozen data

import from "std/memory"

@Memory.frozen
struct AppConfig {
    public host: String
    public port: Integer
}

mut config = AppConfig { host: "localhost", port: 8080 }
config.port = 9090
error[E2030]: cannot mutate field of `@Memory.frozen` struct `AppConfig` via `config` in `main`

Frozen structs are immutable after construction, even through a mut binding, whichever path the mutation takes in progress.

Zero-copy string views

substring returns a StringView, a pointer plus length into the original string's buffer, with no copy:

line = "GET /index.html HTTP/1.1"
path = line.substring(4, 15)     // "/index.html", no allocation

The compiler tracks the view's source and rejects a view that would outlive or observe a changed source (borrowed-view invalidation). This is Lumina's lightweight version of a borrow: it is inferred, and you write no annotations.

Moves for resources

Values with cleanup logic (files, sockets) are moved when they are passed to a take parameter or sent over a channel. Using them afterward is a compile error, for example E5114 for a use after send. Cleanup runs deterministically when the owner goes out of scope, which is the RAII part of the model. Use after send is an error for every value, not only resources in progress.

Where the model is going

The design lives in Language_Memory.md. The GC runtime itself is described in docs/GC_Runtime.md.

Coming from...

Next: Zero-Copy & Servers