Skip to content

A dispatch table: {tag, handler} pairs owned by a receiver

Description

A dispatch table: {tag, handler} pairs owned by a receiver.

A PolyTag says what an item is, not what a receiver should do with it. The same Event means a log line to one Master and a counter bump to another, so the handler cannot belong to the tag. It belongs to the pair (receiver, tag) — which is what this table holds.

Nothing in src/ is needed. PolyHelper.TAG is a value that can be stored, comparing two tags is ==, and Slot reports what the handler did with the item. The table is composed from blocks that already exist.

Not part of matryoshka: the handler's first parameter is the application's own receiver type, which the toolkit cannot name. Applications are free to write a different table, or none at all.

See design/table-dispatch-001.md.

Source

/// Builds a dispatch table type for receiver type `T`.
pub fn TagTable(comptime T: type) type {
    return struct {
        const Self = @This();

        /// A handler for one tag.
        ///
        /// **The transfer rule.** On return, the Slot is null if the handler
        /// took the item, full if it did not. A handler may take the item,
        /// forward it elsewhere, or look and leave it — but it must leave the
        /// Slot telling the truth about which. The caller frees whatever is
        /// left.
        ///
        /// This is a convention between the handler author and the loop that
        /// calls it. Matryoshka cannot enforce it and does not care.
        ///
        /// A handler may both move the item and then fail. The Slot reports
        /// where the item is; the error reports whether the work succeeded.
        /// They are two different questions.
        ///
        /// `anyerror` and not an inferred error set: a function pointer type
        /// needs its error set written out, and the table holds handlers
        /// written by different authors.
        pub const Handler = *const fn (self: *T, slot: *Slot) anyerror!void;

        /// One tag mapped to one handler.
        pub const Entry = struct {
            tag: *const anyopaque,
            handler: Handler,
        };

        /// Usually a comptime literal. May also be a slice of a buffer the
        /// receiver owns — see `design/table-dispatch-001.md`. No allocator
        /// either way, and no `init` or `deinit`: a table is a value.
        entries: []const Entry = &.{},

        /// The handler for `tag`, or null when no entry matches.
        ///
        /// A miss is not a defect. A tag present in one table and absent from
        /// another is the normal result of two receivers doing different jobs.
        pub fn find(self: Self, tag: *const anyopaque) ?Handler {
            for (self.entries) |entry| {
                if (entry.tag == tag) return entry.handler;
            }
            return null;
        }

        /// Calls the handler for the item in `slot`.
        ///
        /// - `error.EmptySlot` — nothing to dispatch.
        /// - `error.NoHandler` — no entry matched. Nothing was called and the
        ///   Slot is untouched, so the caller still holds the item and frees
        ///   it. Unlike the last branch of an `isIt` chain, which cannot.
        ///
        /// Anything else comes from the handler itself. Look at the Slot, not
        /// at the error, to learn where the item went.
        pub fn dispatch(self: Self, receiver: *T, slot: *Slot) anyerror!void {
            const poly = slot.* orelse return error.EmptySlot;
            const handler = self.find(poly.*.tag) orelse return error.NoHandler;
            return handler(receiver, slot);
        }
    };
}

const polynode = @import("matryoshka").polynode;
const Slot = polynode.Slot;