Patterns — Slot and PolyNode Idioms
Concepts: Building Blocks — PolyNode.
API: API Reference — PolyNode, ItemHandle, Slot.
The slot rule in full: API Reference — Tag Identity and Slot Programming.
Slot idioms
Empty Slot initialization
When to use.
- Every acquisition.
Code shape.
Why.
- Every acquisition API requires an empty slot.
- Passing a non-null slot is a programming error.
Slot overwrite prevention
When to use.
- Before every receive/get/create operation.
Code shape.
Why.
- A slot always owns exactly one item.
- Overwriting a non-null slot loses the item it holds.
- Every acquisition API contains this assert. Wrong use panics immediately.
Transfer clears the slot
When to use.
- Every transfer.
Code shape.
or
Why.
- Sender no longer owns the item.
- Cleanup code becomes naturally safe.
- Transfer pre-empts cleanup: a later
defersees null and does nothing.
Null-safe cleanup
When to use.
- Every deferred cleanup.
Code shape.
or
Why.
- Cleanup helpers ignore null slots.
- Cleanup may safely execute after transfer.
Defer-put-early (pool item)
When to use.
- Acquiring a pool item. The defer goes before the get.
Code shape.
var slot: Slot = null;
defer pool.put(ph, &slot); // no-op if slot == null
try pool.get(ph, TAG, .available_or_new, &slot);
// ... work ...
// on transfer: slot = null → defer runs as no-op
// on no transfer: defer recycles item
Why.
- Failure path, success path, and transfer path all become correct automatically.
- If the get fails, the defer sees null — nothing lost.
Example: examples/layer4/018-master_with_pool.zig.
Defer-destroy-early (heap item via PolyHelper)
When to use.
- Creating a heap item. The defer goes before the create.
Code shape.
var slot: Slot = null;
defer EventPolyHelper.destroy(allocator, &slot); // no-op if slot == null
try EventPolyHelper.create(allocator, &slot);
// ... work ...
// on transfer: slot = null → defer runs as no-op
// on no transfer: defer frees item
Example: examples/layer2/097-wake_up_all.zig.
Defer for received mailbox item
When to use.
- Receiving into a slot. Cleanup must cover both the error path and the normal path.
Code shape.
var slot: Slot = null;
defer if (slot) |poly| helpers.freeItem(poly, allocator);
try mailbox.receive(mbh, &slot, null);
// dispatch on slot.?.*.tag, process item
// item stays non-null until explicitly transferred or freed
Example: examples/layer4/031-select_graceful_shutdown.zig.
Fallback destroy after pool.put
When to use.
- Pool may already be closed when the item comes back.
Code shape.
defer EventPolyHelper.destroy(allocator, &slot); // fallback: frees if pool.put left slot non-null
defer pool.put(ph, &slot); // primary: recycles to pool (clears slot on success)
// defers run LIFO: pool.put first, then destroy (no-op if pool.put cleared slot)
Why.
- Pool receives the item if open.
- A closed pool leaves the slot non-null — the caller keeps the item.
- Destroy executes only if the item stayed with the caller.
Example: stories/video_transcoder/video_transcoder.zig.
No raw allocator calls on PolyNode-based types
When to use.
- Every PolyNode-based user type (Event, Sensor, Timer, ShutdownCommand).
Code shape.
// WRONG — raw allocator on PolyNode-based type
const ev = try alloc.create(Event);
// CORRECT — PolyHelper.create/destroy
var slot: Slot = null;
defer EventPolyHelper.destroy(alloc, &slot);
try EventPolyHelper.create(alloc, &slot);
Why.
PolyHelper.createsets the tag and initializes the node.- Raw
allocator.createskips both. The item is unusable for dispatch.
Exempt: mailbox.zig / pool.zig internals, PolyHelper implementations, pool hook bodies, non-PolyNode structs.
Full list: API Reference — Cooperative Cleanup.
PolyNode idioms
Intrusive node embedding
When to use.
- Every PolyNode-based user type, from first definition.
Code shape.
pub const Message = struct {
poly: polynode.PolyNode = .{},
text: []const u8 = "",
priority: u8 = 0,
};
pub const MessagePolyHelper = polynode.PolyHelper(Message);
Why.
PolyNodesits at offset 0. One allocation, no wrapper struct.- Safe cast both ways:
*Messageto*PolyNodeand back, viaPolyHelper. - No separate link object to keep in sync with the payload.
Example: examples/layer1/021-define_type.zig.
PolyHelper everywhere
When to use.
- Every PolyNode type.
Code shape.
Why.
- Eliminates manual tag management.
- Eliminates unsafe casts.
- Eliminates initialization boilerplate.
Node identification
When to use.
- Recovering a concrete type from a
*PolyNodehandle (e.g. anItemHandlereceived from a mailbox or returned by a pool event source).
Code shape.
Why.
- Tag check and recovery are combined.
- Wrong types return null.
Slot identification — accessing owned items
When to use.
- After
createorget, to access fields of the item in a Slot before sending or returning it.
Code shape (assert non-null, known type).
var slot: Slot = null;
defer EventPolyHelper.destroy(allocator, &slot);
try EventPolyHelper.create(allocator, &slot);
EventPolyHelper.mustIdentifySlotAs(&slot).code = 42;
try mailbox.send(mbh, &slot);
Code shape (optional — type may vary).
Why.
- Unwraps the optional internally — no
.?in application code. mustIdentifySlotAspanics if the Slot is empty or the tag does not match.- Use
identifySlotAs(nullable) when the type is not guaranteed.
Polymorphic dispatch
When to use.
- One mailbox or one list carries more than one item type. The receiver recovers the concrete type.
Code shape.
if (EventPolyHelper.identifyNodeAs(handle)) |ev| {
// handle Event
} else if (ShutdownCommandPolyHelper.identifyNodeAs(handle)) |_| {
// handle ShutdownCommand
} else {
// unknown — free and move on
}
identifyNodeAsreturns null on a tag mismatch. Chain calls for each known type.
Example: examples/layer4/031-select_graceful_shutdown.zig, examples/layer4/033-cross_layer_mixed_types_mailbox.zig.
Tag identifies the class
When to use.
- Runtime dispatch.
Pattern.
Not
Use.
- Pointer comparison for infrastructure handles.
- User fields (
kind,role) for application roles.
Details: API Reference — Tag Identity.
Wrapper type for infrastructure handles
When to use.
- Mailbox or Pool must participate in polymorphic dispatch by tag.
Code shape.
const WorkerInbox = struct {
poly: PolyNode,
handle: mailbox.MailboxHandle,
};
pub const WorkerInboxPolyHelper = polynode.PolyHelper(WorkerInbox);
Why.
- Wrapper has its own PolyHelper tag, distinct from
MailboxPolyHelper.TAG. - Enables normal type dispatch. The receiver finds the embedded handle.
Mailbox-as-message
When to use.
- Handing a communication endpoint to another Master.
Pattern.
Typical use.
- Worker completion notification.
- Dynamic topology construction.
- Channel migration.
Worker-finish-signal
When to use.
- A worker signals completion by sending its own mailbox back to the Master.
Pattern.
- Master creates
worker_mbh, spawns a worker viaio.concurrent, passesworker_mbhas parameter. - Worker processes items until a shutdown signal.
- Worker sends
worker_mbhback to the Master's inbox (unclosed) as the finish signal, then exits. - Master confirms class:
mailbox.is_it_you(received.*.tag). - Master confirms instance:
received == worker_mbh(pointer comparison). - Master closes and destroys
worker_mbh, then awaits the worker's future.
Why.
- Replaces relying on the future await as a completion signal, or a separate shutdown message, with handing the mailbox back.
Details: API Reference — Transporting infra handles.
Pool-as-message
When to use.
- Sharing lifecycle managers.
Pattern.
Why.
- PoolHandle is itself a PolyNode.