Patterns — Slot and PolyNode Idioms
Concepts: Tools — 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
or, when the caller takes the item itself:
const ev: *Event = EventPolyHelper.moveFromSlot(&slot) orelse return error.WrongTag;
// slot == null, the caller holds ev
or, when a list takes the item:
Why.
- Sender no longer owns the item.
- Cleanup code becomes naturally safe.
- Transfer pre-empts cleanup: a later
defersees null and does nothing.
Insert from a Slot
When to use.
- Putting an item that sits in a Slot into an
ItemList.
Code shape.
var slot: Slot = null;
try EventPolyHelper.create(allocator, &slot);
EventPolyHelper.mustFromSlot(&slot).code = 7;
list.appendFromSlot(&slot);
// slot == null
prependFromSlot is the same at the front.
Why.
-
appendtakes anItemHandle, so it cannot clear a Slot. Every call site wroteslot = nullon the next line by hand. -
Forget that line and defer-destroy-early frees an item the list still points at.
-
appendFromSlotempties the Slot itself, so there is no line to forget. - The Slot must hold an item. An insert is not a
defertarget, so it followsMbox.sendrather thanPool.put.
Do not.
- Do not use these for a stack item. There is no Slot to empty — use
appendwithtoPoly.
Example: examples/layer1/023-tag_dispatch.zig.
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 pl.put(&slot); // no-op if slot == null
try pl.get(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 mbx.receive(&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 pl.put(&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.
ItemList for many items
When to use.
- Any API that carries more than one item:
receive_batch,close,put_all,on_put,on_close.
Code shape.
var batch: polynode.ItemList = try mbx.receive_batch();
while (batch.popFirst()) |ih| {
const msg = MessagePolyHelper.mustFromPoly(ih);
// ... use msg
}
Why.
-
ItemHandleis one item,Slotis a place for one item,ItemListis many. The trio covers every shape the toolkit passes around. -
popFirstyields anItemHandle, so@fieldParentPtrstays out of your code. -
popFirstcallspolynode.resetbefore it returns. The oldprev/nextlinks are cleared for you — with a rawstd.DoublyLinkedListthey are not,
and that was a documented trap. -
Build one with
.{}andappendFromSlot— see "Insert from a Slot". Take a raw std list over withItemList.moveFromList, which leaves the raw list
empty. -
Under a safety build every insert asserts twice on the item going in: this list does not hold it, and
polynode.is_linkedis false. The list walk
catches a list of one, whichis_linkedmisses;is_linkedcatches a
different list, which the walk cannot reach. Neither alone is complete. -
Take one item out with
remove(ih)— head, middle or tail. It callspolynode.reset, so the item comes back unlinked.popLastispopFirstat
the other end, andfirst/lastlook without taking.
Example: examples/layer2/060-batch_processing.zig.
Details: api/polynode/stdlib-compatibility.md.
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 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.mustFromSlot(&slot).code = 42;
try mbx.send(&slot);
Code shape (optional — type may vary).
Why.
- Unwraps the optional internally — no
.?in application code. mustFromSlotpanics if the Slot is empty or the tag does not match.- Use
fromSlot(nullable) when the type is not guaranteed. - Inspection leaves the Slot full. The item is still there for
sendorput. - To take the item out instead, use
moveFromSlot— see "Transfer clears the slot".
Polymorphic dispatch
Moved. One mailbox or one list carries more than one item type, and the
receiver recovers the concrete type — three ways, and the advice on choosing
between them: Patterns — Dispatch.
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,
mbx: *Mbox,
};
pub const WorkerInboxPolyHelper = polynode.PolyHelper(WorkerInbox);
Why.
- Wrapper has its own PolyHelper tag, distinct from
Mbox.TAG. - Enables normal type dispatch. The receiver finds the embedded
*Mbox.
A mailbox can travel on its own — mbx.toPoly() in, Mbox.mustFromPoly out.
Wrap it only when the receiver needs more than the endpoint: a job id, a
deadline, a reply address alongside it.
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_mbx: *Mbox, spawns a worker viaio.concurrent, passesworker_mbxas parameter. - Worker processes items until a shutdown signal.
- Worker sends
worker_mbx.toPoly()back to the Master's inbox (unclosed) as the finish signal, then exits. - Master confirms class and instance in one step:
Mbox.mustFromPoly(slot.?) == worker_mbx. - Master closes and destroys
worker_mbx, 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.
-
The instance check is a real pointer comparison. Both sides are
*Mbox, so the compiler agrees the two values are the same kind of thing. Under the
handle API this compared two look-alikeItemHandles and only the tag
stood between a match and a silent mistake. -
Mbox.fromPolyis the checking form when a foreign node is possible; it returns null instead of panicking.
Details: API Reference — Transporting infra handles.
Pool-as-message
When to use.
- Sharing lifecycle managers.
Pattern.
Why.
- A pool is itself a PolyNode.
pl.toPoly()in,Pool.mustFromPolyout.