Bbetlang

Arenas

Arenas, generational handles, and checked access.

This is how bet handles memory, and it’s the part that makes the language actually different rather than just funnier. The design position: references are plain data, validity is control flow, and memory lives in arenas. No garbage collector, no borrow checker, no chasing down individual objects to free() them one at a time. The moving parts are all keywords, and they mostly mean what they sound like: crib (arena), cop (allocate), evict (free), tag (handle), holla (checked access), trust (unchecked access), plus ghosted (the dead-or-nil sentinel).

Cribs (crib)

A crib is an arena: a region you allocate into and then free all at once, no roommates negotiating whose turn it is to clean. It comes in two flavors.

Untyped bump crib

crib name reserves a byte arena. cop into it hands back a direct reference you can use right away.

drip Particle { flex x: int, flex y: int }

finna main() {
    crib frame                                   // untyped bump arena
    lowkey a = cop Particle{ x: 1, y: 2 } in frame
    lowkey b = cop Particle{ x: 3, y: 4 } in frame
    spill.it(a.x + b.y)                           // 5
    evict frame                                   // O(1) mass-free
}

Typed slab crib

crib name: T[N] reserves a slab of N fixed-size slots. cop into it hands back a tag (a generational handle) instead of a direct reference, so you reach the data through holla or trust. Cribs can be function-local or module-level.

drip Enemy { flex hp: int }
crib enemies: Enemy[1000]        // module-level typed slab

Late one night at a gas station off the highway I got coffee and grabbed the smallest cup they had, the four-ounce paper one meant for creamer. Then I hit the urn and just didn’t stop. A whole fresh pot, wide open, into this thimble. It crested the rim in about two seconds and kept coming, and the coffee had no interest in staying near the cup. It went across the counter, into the roller-grill napkins, under the lotto machine, off the edge to the floor. I walked out. Whatever the night guy did, the place was spotless by morning, which isn’t the point. The point is a tiny cup doesn’t know it’s a tiny cup, and nothing stops at the rim on its own. A typed slab is you calling the cup size up front: crib enemies: Enemy[1000] is a thousand-slot cup, and the cop that would’ve been number 1001 doesn’t get to flood the store.

Allocating (cop)

cop <init> in <crib> allocates one value into a crib. The initializer is a struct literal or a moods-variant constructor; omitted struct fields zero-default. Into a typed crib, cop returns a tag; into an untyped bump crib it returns a live reference.

lowkey e = cop Enemy{ hp: 50 } in enemies   // e : tag Enemy

Freeing (evict)

Two forms:

  • Whole crib (evict frame): an O(1) mass-free. It reclaims the entire arena and bumps every slot’s generation, so every outstanding tag into it becomes safely dead.
  • Single slot (evict tag in crib): frees one slot and bumps just its generation. The slot is reused by a later cop at a new generation. Evicting a stale or null tag is a safe no-op.

Tags (tag)

A tag T is an 8-byte handle: a slot index plus a generation counter. It’s plain copyable data, so store it in any struct, hold it across frames, pass it between threads, whatever you need. No lifetimes, no borrows, no GC breathing down your neck. What you can’t do is dereference a tag directly, and if you try, the compiler stops you cold. The generation counter is the whole trick for catching dangling references: when a slot gets freed and reused, its generation ticks forward, so an old tag pointing at that reused slot no longer matches and gets caught.

Checked access (hollaghosted)

There was a stretch of a couple years where I was in every group chat and caught the save-the-date for every bachelor party, Orange Beach, Destin, the lake house nobody talks about. Then the invites tapered off. No blowup, no falling out, I’d just see the photos after everyone got home and realize a whole trip had happened without me. The group’s still out there, still getting tagged in a brunch photo every other weekend, and I hear about it Monday like everybody else. I’m still on a list. It’s an old copy of the list, and the real one moved on. That’s ghosted. You holla at your tag expecting the live arm, the slot got reused by somebody with a fresher generation, and the ghosted arm runs instead. No error, nobody crashed. There’s just nobody home at the spot you remember.

holla is the polite, safe way to reach a tag’s data: you holla at the tag and see if anyone’s home. It has two arms. The live arm binds a guaranteed-valid reference, scoped to the block, and the ghosted arm runs when the tag is dead (freed, or its slot got reused). The whole check costs one index plus one integer compare.

finna hpOf(e: tag Enemy) -> int {
    holla r = e in enemies {
        bet r.hp            // r is a live ref, only valid in here
    } ghosted {
        bet -1             // e is dead / reused
    }
}

finna main() {
    lowkey a = cop Enemy{ hp: 30 } in enemies
    spill.it(hpOf(a))       // 30 — live

    evict enemies           // bumps every generation
    spill.it(hpOf(a))       // -1 — `a` now dangles -> the ghosted arm
}

Unchecked access (trust)

The escape hatch for hot loops where you already know, for a fact, that the tag is live. t.trust() in crib resolves straight to a reference with no generation check. Debug builds still check and yeet on a stale tag, so you find out during testing; release builds compile down to a raw indexed load. The syntax is deliberately ugly and easy to grep for, so trust is never something you did by accident.

lowkey e = cop Enemy{ hp: 77 } in enemies
lowkey r = e.trust() in enemies    // no generation check
spill.it(r.hp)                     // 77

The scratch arena (mem.scratch())

A built-in per-frame bump arena, reached as mem.scratch(). It’s auto-evicted at frame end, so per-frame temporaries clean themselves up.

Zero-defaults

When you cop a struct and omit fields (or mem.slab a buffer), everything zero-initializes: integers 0, floats 0.0, bools cap, strings "", nested structs recursively zeroed, and tag, handle, and rawptr fields set to null. Safe to hold, a crash to use.