ferrule

regions

α2
heap-regionarena-regiondevice-regionshared-regionregion-disposaluninitialized-memory

regions

this feature is planned for α2. the spec describes what it will be, not what's implemented now.

regions group allocations under a single lifetime. disposing a region frees everything inside it deterministically. there's no garbage collector.

why regions

traditional memory management has two extremes:

  • manual (c): free each allocation individually, easy to mess up
  • gc (go, java): automatic but unpredictable, can't control when memory is freed

regions are a middle ground:

  • allocate into a region
  • when done, dispose the whole region
  • everything inside is freed at once

this is particularly good for request-scoped data. allocate all the request's memory in one region, dispose when the request finishes.

region kinds

constructordescription
region.heap()general-purpose dynamic region
region.arena(bytes)bump-ptr arena, no individual frees
region.device(id)device memory (dma, gpu)
region.shared()multi-thread access, requires atomics

each call to region.heap() creates a new independent region. there's no global singleton heap.

creation and disposal

const arena = region.arena(1 << 20);  // 1mb
defer arena.dispose();

const buf: View<mut u8> = arena.alloc<u8>(4096);
// use buf...
// arena disposed when scope exits

disposal:

  • frees all allocations in the region
  • runs destructors for capsule values registered with the region
  • returns Unit, never fails
  • errors during finalization are logged but don't propagate

disposing a region invalidates all views bound to it. further access traps.

arenas

arenas are bump allocators. allocation is fast (just increment a pointer), but you can't free individual allocations:

const arena = region.arena(1024 * 1024);  // 1mb
defer arena.dispose();

const a = arena.alloc<u8>(100);   // fast
const b = arena.alloc<u8>(200);   // fast
// can't free a or b individually
// dispose frees everything at once

arenas are great for:

  • parsing (allocate ast nodes, dispose when done)
  • request handling (allocate everything, dispose when request finishes)
  • game frames (allocate per-frame data, reset each frame)

heap regions

heap regions allow individual frees but are slower:

const heap = region.heap();
defer heap.dispose();

const ptr = heap.alloc<u8>(100);
// can use heap.free(ptr) for individual frees
// or let dispose clean everything

transfer between regions

moving data between regions is explicit:

const dst = region.heap();
const moved: View<mut u8> = view.move(buf, to = dst);

moving invalidates the source view. using it after transfer is a compile error.

operationsource afterrequirement
view.copy(src, to)remains validelement type is copyable
view.move(src, to)invalidatedelement type has valid move policy

shared regions

memory in region.shared() may be accessed from multiple threads. mutation requires:

  • atomics effect
  • atomic types or synchronization primitives
const shared = region.shared();
// use with atomic operations only

non-atomic concurrent mutation is undefined behavior.

alloc effect

invoking allocators requires the alloc effect:

function grow_buffer(r: Region, want: usize) -> View<mut u8> effects [alloc] {
  return r.alloc_zeroed<u8>(want);
}

this makes allocation visible in function signatures.

uninitialized memory

ferrule forbids reading uninitialized memory. allocation apis define zero-init policy:

apibehavior
alloc_zeroed<T>(...)returns zeroed memory
alloc_uninit<T>(...)returns View<Uninit<T>>

uninitialized views must be fully initialized before use:

const un: View<Uninit<u32>> = region.heap().alloc_uninit<u32>(4);
// initialize all elements...
const init: View<u32> = view.assume_init(un);  // only legal when fully initialized

this prevents undefined behavior from reading garbage.

device regions

region.device(id) exposes device memory:

const gpu = region.device(gpu_id);
const gpu_buf = gpu.alloc<f32>(1024);

// host access may be illegal without mapping
device.copy_to_host(gpu_buf, host_buf);
host.copy_to_device(host_buf, gpu_buf);

transfers can fail and return errors.

regions and scoped ownership

regions work with scoped ownership:

  • views can't escape their creation scope
  • regions created inside a scope are disposed when the scope exits
  • if you pass a region out, you own disposal
function process() -> View<u8> {
    const arena = region.arena(1024);
    defer arena.dispose();
    
    const buf = arena.alloc<u8>(100);
    return buf;  // error: buf escapes its region
}

to return data, copy it:

function process(out_region: Region) -> View<u8> effects [alloc] {
    const arena = region.arena(1024);
    defer arena.dispose();
    
    const buf = arena.alloc<u8>(100);
    // ... process buf ...
    return view.copy(buf, to = out_region);  // ok: copy to caller's region
}

example: request-scoped allocation

function handleRequest(req: Request, cap io: Io) -> Response 
    error RequestError 
    effects [alloc, io] 
{
    const arena = region.arena(1 << 20);  // 1mb for this request
    defer arena.dispose();
    
    const body = check parseBody(req, arena);
    const result = check process(body, arena);
    const response = check serialize(result, arena);
    
    // copy response to caller's region before returning
    return response.clone();
}
// arena disposed here, all request memory freed

On this page