Skip to content

Table dispatch — the third way, where the choice is data

Description

Table dispatch — the third way, where the choice is data.

023 and 026 write the choice as code. This one writes it as a value.

  • 023 has the item and asks each type to cast it: fromPoly.
  • 026 has the tag and asks each type to confirm it: isIt.
  • Both put the choice in the chain, so the choice is fixed where the chain is written. Two receivers that treat an Event differently write two chains.

  • A table is {tag, handler} pairs the receiver owns. Change the table and the same tag reaches a different handler.

  • Push an Event, a Sensor and a Timer into one mixed list.

  • Dispatch each through a table that names all three.
  • Then dispatch an Event through a second table, which maps the same tag to a different handler. No chain can express that.

  • A tag no table names gives error.NoHandler with the item still in the Slot, so the caller frees it. The last branch of a chain cannot.

Diagram

 alloc.create (Event, Sensor, Timer)  ──► list
      │ list.popFirst
 table.dispatch(&recorder, &slot)
      │ find: entry.tag == poly.tag
 handler(recorder, slot) ──► Slot says where the item went
      │ freeSlot per item

Source

/// A receiver. Its handlers look at the item and leave it in the Slot.
const Recorder = struct {
    events: usize = 0,
    sensors: usize = 0,
    timers: usize = 0,
    marked: usize = 0,
    last_code: i32 = 0,

    fn onEvent(self: *Recorder, slot: *Slot) anyerror!void {
        // The table matched the tag, so this cast cannot fail.
        self.last_code = items.Event.EventPolyHelper.mustFromSlot(slot).*.code;
        self.events += 1;
    }

    fn onSensor(self: *Recorder, slot: *Slot) anyerror!void {
        const sn = items.Sensor.SensorPolyHelper.mustFromSlot(slot);
        try helpers.expect(error.TableDispatchFailed, sn.*.value == 2.71, "wrong sensor value");
        self.sensors += 1;
    }

    fn onTimer(self: *Recorder, _: *Slot) anyerror!void {
        // A handler that never reaches the item. The tag was enough.
        self.timers += 1;
    }

    /// The second table's handler for an Event. Same tag, other work.
    fn markEvent(self: *Recorder, _: *Slot) anyerror!void {
        self.marked += 1;
    }
};

const Table = helpers.TagTable(Recorder);

/// The table is a value, so it is a container-level const. Every Recorder
/// shares it and nothing builds it at start-up.
const record_table: Table = .{ .entries = &.{
    .{ .tag = items.Event.EventPolyHelper.TAG, .handler = Recorder.onEvent },
    .{ .tag = items.Sensor.SensorPolyHelper.TAG, .handler = Recorder.onSensor },
    .{ .tag = items.Timer.TimerPolyHelper.TAG, .handler = Recorder.onTimer },
} };

/// A second table over the same receiver type. The Event tag is in both,
/// against a different handler. The Sensor tag is in neither — a receiver
/// with no handler for a type is a normal state of affairs.
const mark_table: Table = .{ .entries = &.{
    .{ .tag = items.Event.EventPolyHelper.TAG, .handler = Recorder.markEvent },
} };

pub fn table_dispatch_loop(allocator: std.mem.Allocator, io: std.Io) !void {
    _ = io;
    var list: polynode.ItemList = .{};

    defer items.freeList(&list, allocator);

    {
        var slot: Slot = null;
        try items.Event.EventPolyHelper.create(allocator, &slot);
        items.Event.EventPolyHelper.mustFromSlot(&slot).code = 7;
        list.appendFromSlot(&slot);
    }

    {
        var slot: Slot = null;
        try items.Sensor.SensorPolyHelper.create(allocator, &slot);
        items.Sensor.SensorPolyHelper.mustFromSlot(&slot).value = 2.71;
        list.appendFromSlot(&slot);
    }

    {
        var slot: Slot = null;
        try items.Timer.TimerPolyHelper.create(allocator, &slot);
        list.appendFromSlot(&slot);
    }

    var recorder: Recorder = .{};

    while (list.popFirst()) |poly| {
        var slot: Slot = poly;

        // Covers every outcome. The handler may take the item, forward it,
        // or leave it — this frees whatever is left, and does nothing when
        // the Slot is null.
        defer items.freeSlot(&slot, allocator);

        try record_table.dispatch(&recorder, &slot);
    }

    try helpers.expect(error.TableDispatchFailed, recorder.events == 1, "wrong event count");
    try helpers.expect(error.TableDispatchFailed, recorder.sensors == 1, "wrong sensor count");
    try helpers.expect(error.TableDispatchFailed, recorder.timers == 1, "wrong timer count");
    try helpers.expect(error.TableDispatchFailed, recorder.last_code == 7, "wrong event code");

    try secondTable(allocator, &recorder);
}

/// The same tag, the other table, the other handler.
fn secondTable(allocator: std.mem.Allocator, recorder: *Recorder) !void {
    {
        var slot: Slot = null;
        defer items.freeSlot(&slot, allocator);

        try items.Event.EventPolyHelper.create(allocator, &slot);
        try mark_table.dispatch(recorder, &slot);

        // The Event tag reached markEvent, not onEvent. The item is the
        // same one record_table would have sent to onEvent.
        try helpers.expect(error.TableDispatchFailed, recorder.marked == 1, "wrong marked count");
        try helpers.expect(error.TableDispatchFailed, recorder.events == 1, "onEvent ran again");
    }

    {
        var slot: Slot = null;
        defer items.freeSlot(&slot, allocator);

        try items.Sensor.SensorPolyHelper.create(allocator, &slot);

        // mark_table has no entry for a Sensor. Nothing is called and the
        // item never leaves the Slot, so the defer above frees it. The
        // last branch of an isIt chain has no type and cannot.
        const missed = mark_table.dispatch(recorder, &slot);
        try helpers.expect(error.TableDispatchFailed, missed == error.NoHandler, "expected NoHandler");
        try helpers.expect(error.TableDispatchFailed, slot != null, "miss took the item");
    }
}

const items = @import("../items/items.zig");
const helpers = @import("../helpers/helpers.zig");
const polynode = @import("matryoshka").polynode;
const std = @import("std");
const Slot = polynode.Slot;