Skip to content

Box Resource

Accepted

Accepted for the V1 owning heap resource and owned dynamic contract value model; helper signatures use the accepted V1 generic-function and generic-impl syntax.

Box(T) is the standard owning heap-allocation resource:

fn Box(comptime T: Type) => @resource struct(pub Disposable) {
  fn create(alloc: *impl Allocator, value: T) Self!AllocError

  fn create_from_dyn(comptime I: Type, alloc: *dyn Allocator, value: I) Self!AllocError
    if T.is_dyn()

  fn ptr(self: *Self) *T
  fn ptr_const(self: *const Self) *const T

  fn into_dyn(self: Self, comptime C: Type) Box(dyn C)

  fn upcast(self: Self, comptime C: Type) Box(dyn C)

  fn dispose(self: *Self) void
}

Box(T).create(alloc, value) is the real constructor. Box itself is a comptime type factory function, not a namespace with uninstantiated static methods. The ergonomic prelude helper is a free function:

fn box(comptime T: Type, alloc: *impl Allocator, value: T) Box(T)!AllocError
{
  return try Box(T).create(alloc, value)
}

box(T, alloc, value) is the V1 ergonomic helper for Box(T).create(alloc, value). Inferring T from value is deferred to CEP-0016: Generic Parameter Inference.

The prelude helper for direct owned erasure is:

fn box_dyn(comptime C: Type, comptime I: Type, alloc: *impl Allocator, value: I) Box(dyn C)!AllocError
  if I.implements(C)
{
  var concrete = try Box(I).create(alloc, value)
  return (move concrete).into_dyn(C)
}

The generic-helper signatures shown here use the accepted V1 generic-function spelling.

Fixed box_dyn semantics:

  • C is a dyn-safe constraint type, possibly an intersection.
  • value has a concrete type I.
  • I implements C.
  • I must not be an already-erased box type such as Box(dyn D).
  • the result is Box(dyn C).

Resource-owning concrete types are allowed dynamic payloads when they satisfy the normal box_dyn rules. The rejection is specific to already-erased dynamic box owner values, because passing Box(dyn D) would box the owner instead of reshaping the existing erased payload.

Future function overloading and generic parameter inference may allow box(alloc, value) and box_dyn(C, alloc, value) to become overloads of a single box name. See CEP-0004: Function Overloading and CEP-0016: Generic Parameter Inference. Until that design is settled, V1 keeps the direct-erasure helper explicitly named box_dyn and spells its concrete implementer type argument explicitly.

Allocator is a semantic contract. Box(T).create, box, and box_dyn use static allocator dispatch through alloc: *impl Allocator, so the concrete allocator implementation remains visible for specialization and inlining at the allocation call. Box(dyn C).create_from_dyn(I, alloc, value) is the explicit dynamic-allocator constructor for APIs that already operate behind an erased allocator boundary, such as dynamic iteration.

Ownership and Disposal

Box is a public resource type and publicly implements Disposable.

Box.dispose() owns payload cleanup:

  • if the payload type implements Disposable, the payload is disposed before storage is freed.
  • otherwise storage is freed directly.
  • for Box(dyn C), cleanup uses box-owned dispose metadata for the original concrete payload.
  • dynamic box cleanup does not require C to include Disposable.

Box.dispose() does not require an Allocator argument at the call site. Box stores enough private freeing state when it is created to release its allocation later.

That physical state is representation-defined. It may be an allocator pointer plus static freeing descriptor, arena token, free function, global allocator singleton, or no stored field for a known allocator strategy. The allocator authority used for creation must remain valid for every live box that depends on it unless the concrete allocator documents a stronger free-independent policy. This applies to both static Box(T).create and erased-allocator Box(dyn C).create_from_dyn.

Box(T) is always non-copy, even when T is copyable. Copying a box would duplicate ownership of the same allocation. Ownership transfer uses move:

var a = try Box(i32).create(alloc, 42)
var b = move a // ok
defer b.dispose()

Copying a Box(T) is invalid:

var a = try Box(i32).create(alloc, 42)
var b = a      // error

Construction

Box(T).create(alloc, value) boxes a sized concrete T. The allocator parameter has type *impl Allocator, the function-parameter anonymous static constraint form, meaning a pointer to a compile-time-known concrete allocator implementation. The allocation call is statically dispatched; the resulting box may erase the allocator identity into private freeing state so that dispose() stays allocator-argument-free.

The allocator-lifetime rule is an ownership convention and lint candidate, not a V1 hard lifetime-proof system. Obvious local cases where a box may outlive borrowed allocator authority are covered by ownership/allocator-authority-escape.

Call entry rules:

  • the call owns the parameter value under normal by-value and move rules.
  • passing a named non-copy value requires move.
  • passing a named copyable value may copy it into the parameter.

Allocation success:

  • ownership of the parameter value moves into the allocation.
  • source semantics do not require an observable stack value followed by a heap move.
  • lowering may allocate first and initialize the payload directly in heap storage for rvalues, aggregates, and temporaries.
  • normal call evaluation order still applies.
  • direct-to-heap construction is an optimization only when it preserves observable value-expression effects, errors, and cleanup.

Allocation failure:

  • Box(T).create disposes the parameter value when T implements Disposable, then returns AllocError.
  • otherwise the parameter lifetime ends with no cleanup step.
  • failed boxing of a Disposable payload destroys that parameter value.
  • if the call copied a copyable Disposable value into the parameter, only that copied parameter value is disposed.
  • the original caller-owned value remains separate and remains the caller's cleanup responsibility.

Payload Forms

In V1, Box(T) accepts only:

  • sized concrete payload types
  • dyn C payloads

Constructor rules:

  • Box(T).create(alloc, value) is callable only when T is a sized concrete type.
  • Box(dyn C).create(alloc, value) is not a V1 constructor because there is no inline dyn C value to pass by value.
  • owned dynamic boxes are constructed only by box_dyn(C, I, alloc, value), Box(dyn C).create_from_dyn(I, alloc, value), (move Box(I)).into_dyn(C), or (move Box(dyn D)).upcast(C).
  • other unsized payload families are not valid Box payloads in V1; they need separate metadata, allocation layout, construction, and destruction rules.

Dynamic Box Construction

Box(dyn C) is created from a sized concrete implementer of a semantic contract constraint, not by directly constructing an inline dyn C value.

Rules:

  • C cannot be a structural constraint from satisfies(...).
  • use box_dyn(C, I, alloc, value), use Box(dyn C).create_from_dyn(I, alloc, value) when the allocator is already erased as *dyn Allocator, or create Box(I) with Box(I).create(alloc, value) and convert it with into_dyn(C).
  • box_dyn(C, I, alloc, value) requires value to have concrete sized implementer type I.
  • box_dyn(C, I, alloc, value) rejects already-erased box owner values such as Box(dyn D); use (move box).upcast(C) for dyn-to-dyn payload reshaping.
  • Box(dyn C).create_from_dyn(I, alloc, value) has the same payload rules as box_dyn(C, I, alloc, value) but takes alloc: *dyn Allocator.
  • concrete resource-owning payload types are still valid when they satisfy the normal box_dyn rules; the rejection is specific to already-erased dynamic boxes.
  • erasure from arbitrary unsized implementers is deferred.
  • semantically, box_dyn(C, I, alloc, value) and Box(dyn C).create_from_dyn(I, alloc, value) behave as if they create Box(I) and then convert that box with into_dyn(C).

Failure and lowering:

  • allocation failure before a box exists follows Box(I).create(alloc, value) failure semantics.
  • descriptor construction is comptime/constant in V1, so box_dyn has no ordinary fallible descriptor-initialization step after allocation succeeds.
  • box_dyn either returns a fully initialized Box(dyn C) or returns AllocError after cleaning up any owned parameter or partial payload state and freeing any allocation.
  • implementations may fuse this into one allocation, write the payload directly into final storage, and initialize the final erased descriptor without materializing an intermediate concrete box.
  • fused lowering must preserve the same failure cleanup behavior.
  • the fused path does not need to use the same physical descriptor layout as the unfused sequence because Box layout is private.

box_dyn, Box(dyn C).create_from_dyn, and Box(T).into_dyn use the general dynamic contract metadata reflection API, such as I.dyn_metadata(C), to obtain compiler-checked dispatch metadata and dispose metadata. This keeps Box implemented in the prelude without making Box itself compiler magic. Prelude code stores and uses the returned metadata; it does not manually construct contract vtables.

Box does not forward methods to the contained value in V1. Borrow the payload explicitly:

var boxed = try Box(Buffer).create(alloc, buffer)
defer boxed.dispose()

var p = boxed.ptr()
p.clear()

Calling ptr() or ptr_const() follows the ordinary method receiver rules; there is no Box-specific compiler knowledge. Because both methods have pointer receivers, the box receiver must be an addressable place in V1:

Box(Buffer).create(alloc, buffer).ptr() // error: rvalue receiver is not auto-addressed

Use a named box binding when borrowing from it:

var boxed = try Box(Buffer).create(alloc, buffer)
defer boxed.dispose()
var p = boxed.ptr() // ok

For Box(dyn Contract), ptr returns a borrowed dynamic pointer:

var it_box = try box_dyn(Iterator(*const T), IndexIterator(Self, T), alloc, iter)
defer it_box.dispose()

var it = it_box.ptr()
while const item = it.next() {
}

ptr() requires mutable access to the box and yields *dyn C, allowing dynamic operations whose receiver is self: *Self. ptr_const() requires only readonly access to the box and yields *const dyn C, allowing only readonly dynamic operations whose receiver is self: *const Self. A const box binding may call ptr_const(), and a mutable box may still be borrowed read-only through ptr_const().

For a dynamic box, ptr() returns a borrowed fat pointer value containing the dynamic dispatch metadata needed for calls. Semantically, Box forms that pointer through compiler-produced pointer descriptor behavior rather than assembling fat-pointer fields itself.

The implementation may store vtable pointer(s), a compact descriptor, inline descriptor fields, or specialize for a known contract shape. The storage shape is private; the requirement is that ptr() and ptr_const() produce valid borrowed dynamic pointers for the box's erased contract.

The box keeps separate dispose behavior. Moving the box moves only the owning descriptor, not the heap allocation, so existing payload pointers are not invalidated by the move itself; they become invalid when the owning box is disposed. The moved-from binding is still unusable under normal move rules, and ordinary aliasing/lifetime rules still apply to any outstanding borrow. ptr() and ptr_const() do not create Box-specific or dyn-specific borrow tracking; interactions between mutable payload borrows and outstanding readonly payload borrows follow the general pointer aliasing model and optional lint policy.

into_dyn consumes an existing concrete box and returns an owning dynamic box without allocating, reallocating, or moving the payload:

var concrete = try Box(IndexIterator(Self, T)).create(alloc, iter)
var erased = (move concrete).into_dyn(Iterator(*const T))

The destination contract C is explicit in V1; into_dyn does not infer it from the expected return type. into_dyn is available on Box(I) where I is a concrete sized implementer of C; it is not the dyn-to-dyn conversion operation for Box(dyn D).

If the boxed payload type I is itself an owner type such as Box(dyn D), into_dyn(C) still follows the ordinary Box(I) rule when that owner type visibly implements C. This erases the owner value as the payload; it does not reshape the hidden payload inside the nested dynamic box.

The moved receiver is the box value. The heap allocation and payload stay in place; only the owning descriptor changes to carry dynamic dispatch and dispose metadata. If an implementation cannot represent the target Box(dyn C) without allocation, that representation is not acceptable for V1 Box.

box_dyn(C, I, alloc, value) has the same source semantics as creating Box(I) for the concrete type I of value, then converting that box with into_dyn(C), even when optimized into a single allocation.

upcast consumes an existing dynamic box and narrows or normalizes its represented erased surface to a base/dependency contract, normalized intersection subset, or equivalent normalized spelling without allocating or reallocating. Owned dynamic box upcasts are never implicit; source must use explicit consuming upcast(C).

var derived: Box(dyn MutableSequence(f32)) = ...
var base = (move derived).upcast(Sequence(f32))

The destination contract C is explicit in V1; upcast does not infer it from the expected return type. Upcast validity is checked at compile time from the represented erased contract type and C; upcast is not a runtime-fallible dynamic cast.

Allowed dyn-to-dyn conversions:

  • base/dependency upcasts, such as Box(dyn MutableSequence(T)) to Box(dyn Sequence(T))
  • intersection subset narrowing, such as Box(dyn (A & B)) to Box(dyn A)
  • same-surface target spellings whose source and target normalize to the same component set, such as Box(dyn (A & B)) to Box(dyn (B & A)) or a target that redundantly spells an implied base

These conversions select represented vtable/component metadata; they do not re-check the hidden concrete payload for unrelated conformances. The conversion must be allocation-free. If an implementation cannot represent the target Box(dyn C) without allocation, that representation is not acceptable for V1 Box.

upcast moves only the owning descriptor and preserves the payload allocation, so outstanding payload borrows are not physically invalidated by the upcast itself; disposal still invalidates them, and ordinary owner/borrow rules still govern use.

upcast does not sidecast to unrelated contracts the hidden concrete type might implement, such as Box(dyn A) to Box(dyn B). Sidecasts require runtime type identity and dynamic conformance lookup, which are deferred.

Upcasts follow declared contract dependencies and represented intersection components only. They do not infer conceptual mutable-to-readonly relationships across different applied contract types. For example, Box(dyn MutableIterable(*T)) does not upcast to Box(dyn Iterable(*const T)) in V1 unless a contract family explicitly declares that dependency.

Upcasting a dynamic box changes the erased payload surface owned by the box; it does not add owner forwarding. For example, Box(dyn MutableSequence(T)) may upcast to Box(dyn Sequence(T)), but sequence operations still require an explicit borrowed dynamic pointer:

var base = (move derived).upcast(Sequence(T))
const n = base.ptr_const().len()

Box(dyn D).dyn_metadata(C) is not the dyn-to-dyn conversion path for the hidden payload. If called at all, it asks whether the box owner type itself visibly implements C, such as one of the closed prelude forwarding implementations. Reshaping the boxed erased payload uses upcast(C).

Inherited dynamic operations are visible through that borrowed pointer. For example, because Sequence(T) depends on Iterable(*const T), a boxed dynamic sequence can request dynamic iteration by borrowing the payload:

var it = try base.ptr_const().iter_dyn(alloc)
defer it.dispose()

If the owner itself should expose the boxed iterable forwarding surface, the owner must be explicitly upcast to that surface first:

var iterable = (move base).upcast(Iterable(*const T))
var it = try iterable.iter_dyn(alloc)
defer it.dispose()

The direct call base.iter_dyn(alloc) remains rejected because Box(dyn Sequence(T)) is not one of the closed V1 boxed forwarding implementations.

Box(dyn Contract) stores dispose metadata independently from visible dyn vtables. The canonical dynamic metadata model is documented in Dynamic Contract Metadata.

Box-specific cleanup rules:

  • the vtable describes callable contract operations.
  • dispose metadata ends the original concrete payload lifetime.
  • Box stores allocator/freeing state separately because Box owns the heap allocation.
  • separating payload disposal from storage freeing keeps cleanup correct after upcast and keeps payload disposal metadata reusable by other owners.

The visible erased contract surface may include Disposable, such as Box(dyn (Readable & Disposable)). That only controls which dynamic methods can be called through a borrowed pointer. It does not replace box-owned cleanup metadata, and ptr() / ptr_const() do not special-case or reject visible Disposable surfaces. Calling dispose through a borrowed dynamic pointer to a payload still owned by Box risks double cleanup when the box is later disposed; this is covered by ownership/dispose-borrowed-box-payload.

For a dynamic box, dispose() invokes the stored dispose descriptor to destroy the payload, then uses the box-owned private freeing state to free storage. The caller does not provide an allocator to dispose; cleanup is driven entirely by the owning box descriptor. The dispose descriptor does not free storage and should lower to at most a null check plus an indirect function call, with no-op and direct-call optimizations allowed when statically known.

dispose(self: *Self) requires mutable access to the box because it ends the box value's lifetime and invalidates the stored data pointer and descriptors. This applies to Box(T) and Box(dyn C) alike.

After dispose, the old box value must not be read or used. A var storage slot that held a box may be assigned a fresh Box(T) or Box(dyn C) value after the old value has been disposed.

Box(dyn C) has no public field layout. The implementation may store a nullable dispose function, a compact no-op sentinel, a richer unforgeable descriptor, or omit dispose state when it is statically known to be unnecessary. The semantic requirement is that dispose() performs the correct payload disposal before freeing storage.

The performance target for Box(dyn C) is the same shape an experienced C programmer would write by hand. For a single-contract dynamic box, lowering should be able to use a representation equivalent to:

data: *opaque
vtable: *const C_vtable
dispose_fn: ?fn(*opaque) void
free_state: <private representation>

For an intersection such as Box(dyn (A & B)), lowering may store one vtable pointer per normalized contract component, or an equivalently efficient compact descriptor. There must be no runtime reflection object, heap allocation for descriptors, hash-map lookup, or dynamic type lookup on ordinary ptr() / dispose() paths.

Box(dyn Contract) has no public layout guarantee in V1. Small-object optimization, runtime downcasting, concrete type identity metadata, and stable ABI layout for owned dynamic values are deferred. Runtime identity and dyn casts are tracked by CEP-0006: Runtime Type Identity and Dyn Casts.

Boxed Dynamic Iteration

Boxed iterator ownership and the closed V1 forwarding surface for Box(dyn Iterator), Box(dyn Iterable), and Box(dyn MutableIterable) are documented in Boxed Iterator Forwarding.