The Zero-GC Memory Model: Arenas & Pools

Ube completely eliminates Garbage Collection pauses without imposing Rust's lifetime annotations ('a). Memory safety is achieved through deterministic bump arenas, explicit boundary promotion, and generational handle pools.

1. The Zero-GC Philosophy

Garbage-collected languages (Go, Java, C#) suffer from unpredictable p99 latency spikes and stop-the-world pauses that disrupt 60 FPS / 120 FPS game rendering loops and audio pipelines. Manual memory management (C, C++) invites memory leaks, buffer overflows, and use-after-free exploits.

Ube solves this with three distinct tiers:

  1. Stack: Fixed-size primitives, arrays, and value structs.
  2. Regional Bump Arenas (arena { }): Dynamic collections and strings whose memory is reclaimed in 1 single CPU instruction.
  3. Generational Pools (Pool[T]): Dynamic entities and graphs with compile-time affine ownership.

2. Regional Arenas (arena { ... })

An arena block establishes a new lexical bump allocator. Every heap allocation (such as dynamic slices []T, string builder buffers, and newly formatted strings) made inside the block is allocated from this child arena.

When execution exits the block, the entire scratch memory is reclaimed instantly by rewinding the bump pointer:

fn main() {
    println("Entering main arena scope...")

    // Statement Form:
    arena {
        let mut scratch_items = []i32{}
        for i in 0..1000 {
            scratch_items.push(i * 10)
        }
        println(f"Processed {scratch_items.len} items")
    } // <-- scratch_items memory is reclaimed instantly here!

    println("Exited arena: zero garbage left behind.")
}

Arenas can also be used as expressions, returning a computed value:

fn compute_summary() -> i32 {
    let total = arena {
        let mut data = []i32{10, 20, 30}
        data[0] + data[1] + data[2] // Primitive integers are flat value types!
    }
    return total
}

3. The Upward Reference Invariant

Because child arenas are wiped before outer regions, an inner arena must never store data into an outer variable without promoting it. The Ube compiler enforces this strictly at compile time:

fn leak_attempt() {
    let mut outer_list = []i32{}

    arena {
        // COMPILE ERROR: Outer slice mutation attempted within inner arena
        // outer_list.push(42) 
        
        // COMPILE ERROR: Assigning inner-allocated data to outer variable
        // outer_list = []i32{1, 2, 3} 
    }
}
⚠️ Upward Reference Rule
Data allocated inside a child arena cannot escape into an outer parent region unless it is explicitly deep-copied using promote(...).

4. The promote(value) Primitive

To return or transfer heap-backed data (such as slices or strings) out of an arena block, call promote(value). Promotion performs an explicit deep copy of the value into the parent region's arena:

fn load_active_ids() -> []i32 {
    let result = arena {
        let mut temp = []i32{}
        temp.push(101)
        temp.push(102)
        temp.push(103)

        // Deep copies the slice into the parent arena:
        promote(temp)
    } // temp's scratch memory is freed; promoted slice survives!

    return result
}

fn main() {
    let ids = load_active_ids()
    println(f"Loaded {ids.len} IDs: first={ids[0]}")
}

5. Generational Entity Pools (Pool[T])

In games and UI applications, game objects and entities (enemies, projectiles, particles) are constantly spawned and destroyed in arbitrary order. Bump arenas are unsuitable for arbitrary per-entity lifecycles.

For this domain, Ube provides Pool[T]—a high-performance, generational slot allocator:

struct Enemy {
    id: i32,
    hp: i32,
    active: bool
}

fn main() {
    // 1. Create a pool with 128 initial slots
    let mut enemies = Pool[Enemy].new(128)

    // 2. Spawn an entity and receive a generational handle
    let boss_handle = enemies.spawn(Enemy {
        id: 1,
        hp: 500,
        active: true
    })

    // 3. Retrieve entity using generational lookup (returns Enemy?)
    if let e = enemies.get(boss_handle) {
        println(f"Boss #{e.id} is alive with {e.hp} HP")
    }

    // 4. Despawn entity linearly
    enemies.despawn(boss_handle)

    // 5. Attempting to use boss_handle again:
    // enemies.get(boss_handle) 
    // ^-- COMPILE ERROR: Use of moved variable 'boss_handle'
}
💡 Why Generational Handles?
Generational index handles contain both an index and a generation counter. If a slot is reused for a new entity, stale handles from older entities fail lookup safely (returning null) rather than reading corrupted memory.

6. Affine Move Semantics

Non-copy types in Ube (such as string, dynamic slices []T, and Pool[T] handles) have single ownership. Assigning them transfers ownership (move):

fn main() {
    let a = "Ube Core String"
    let b = a // Ownership of string moved to 'b'

    // println(a) // COMPILE ERROR: use of moved variable 'a'
    println(b)    // OK

    let mut message = "first"
    let snapshot = message  // 'message' moved
    message = "second"      // Reassignment reactivates 'message'
    println(message)        // OK
}