Skip to content

Migrate your code

You have a std list. You want the type check.

The change is small and boring.

  • You find and replace, five times.
  • There is no design to think about.
  • The list itself does not change: the same std type, the same calls.

Before and after

The std Node moves into a TypedNode.

The TypedNode holds the Node and the type id.

before                       after
Message                      Message
+--------------------+       +---------------------------+
| text               |       | text                      |
| node  <-- the list |       | tnode: DoublyTypedNode    |
+--------------------+       | +-----------------------+ |
                             | | node   <-- the list   | |
                             | | type id               | |
                             | +-----------------------+ |
                             +---------------------------+
NAQ: Why not keep the type id in the std Node?

The std Node belongs to std. Paternitas does not change std.

So the TypedNode holds the std Node, and the type id next to it.

Your list still links plain std Nodes.

Before, with the plain std list:

const Message: type = struct {
    text: []const u8,
    node: std.DoublyLinkedList.Node = .{},
};

const Job: type = struct {
    id: u32,
    node: std.DoublyLinkedList.Node = .{},
};
var message: Message = .{ .text = "hello" };
var job: Job = .{ .id = 42 };

var list: std.DoublyLinkedList = .{};
list.append(&message.node);
list.append(&job.node);
const first: *std.DoublyLinkedList.Node = list.popFirst() orelse return error.ListEmpty;
const m: *Message = @fieldParentPtr("node", first);
std.log.info("message: {s}", .{m.*.text});

const second: *std.DoublyLinkedList.Node = list.popFirst() orelse return error.ListEmpty;
const j: *Job = @fieldParentPtr("node", second);
std.log.info("job: {d}", .{j.*.id});

After, with Paternitas:

const Message: type = struct {
    text: []const u8,
    tnode: paternitas.DoublyTypedNode = .{},
};
const TypedMessage: type = paternitas.Typed(Message);
var message: Message = .{ .text = "hello" };
TypedMessage.setTypeId(&message);
var list: std.DoublyLinkedList = .{};
list.append(TypedMessage.node(&message));

const node: *std.DoublyLinkedList.Node = list.popFirst() orelse return error.ListEmpty;
const recovered: *Message = TypedMessage.parentFromNode(node) orelse return error.WrongParent;

The full program: One struct, one list.


The five steps

Do them for each struct that goes into the list.

step find replace with
1 std.DoublyLinkedList.Node paternitas.DoublyTypedNode
1 std.SinglyLinkedList.Node paternitas.SinglyTypedNode
2 the end of the struct, }; };, then const TypedMessage = paternitas.Typed(Message);
3 after var message: Message = .{ ... }; add TypedMessage.setTypeId(&message);
4 &message.node TypedMessage.node(&message)
5 @fieldParentPtr("node", n) TypedMessage.parentFromNode(n), or TypedMessage.mustParentFromNode(n)

Job gets the same five steps, with TypedJob.

Step 1 keeps the field name, or changes it. Both are fine.

Step 3 has one more place.

  • Your code may write the whole struct somewhere, as a reset.
  • Add setTypeId right after that write too.
  • The setTypeId rule says when to call it.

What the compiler finds

The compiler stops at each place you missed in steps 4 and 5.

  • &message.node no longer exists. The field is a TypedNode now.
  • @fieldParentPtr("node", n) no longer matches the field.

It cannot find a missed step 3.

  • parentFromNode gives null for that struct.
  • mustParentFromNode panics, in every build mode.
  • The panic says what it found: <no type>.
mustParentFromNode: asked for must_parent_from_node.Msg, found <no type>

That panic comes from this program:

//! mustParentFromNode panics in every build mode when setTypeId was never
//! called on the Parent.

const Msg = struct { tnode: p.SinglyTypedNode = .{} };

pub fn main() void {
    var m: Msg = .{};
    _ = p.Typed(Msg).mustParentFromNode(p.Typed(Msg).node(&m));
}

const p = @import("paternitas");

When you see <no type>, add setTypeId right after that struct is created.


The four calls

You use these most of the time.

call what it does
paternitas.Typed(Message) makes the helper for Message
TypedMessage.setTypeId(&message) sets the type id of Message
TypedMessage.node(&message) gives the Node to the std list
TypedMessage.parentFromNode(node) checks the Node and gives you *Message, or null
NAQ: What does a check cost?

One pointer compare.

There is a fifth call for one case.

  • TypedMessage.mustParentFromNode(node) gives you *Message, or panics.
  • Use it when another type would be a bug.
  • It panics in every build mode.
  • The panic message shows both type names.
NAQ: Does it check in ReleaseFast?

Yes.

The type check works in every build mode.

The must calls panic in every build mode too.

Several types in one list? Use parentFromNode, and ask each type in turn.

Several types in one list shows how.


Singly or doubly linked?

Use the typed Node that matches your list.

your list the field in your struct
std.SinglyLinkedList tnode: paternitas.SinglyTypedNode = .{},
std.DoublyLinkedList tnode: paternitas.DoublyTypedNode = .{},

STNode and DTNode are short names for the same two types.

The field name does not matter.

  • node, tnode, link. All are fine.
  • The field can be anywhere in the struct.
  • Paternitas finds the field by its type.

Do not touch the fields inside the TypedNode. Use the calls.

One struct has one TypedNode.

A second one is a compile error:

//! Two TypedNodes in one struct do not compile.

const TwoTypedNodes = struct {
    a: p.SinglyTypedNode = .{},
    b: p.DoublyTypedNode = .{},
};

comptime {
    _ = p.Typed(TwoTypedNodes).typeId();
}

const p = @import("paternitas");
two_typed_nodes.TwoTypedNodes: more than one TypedNode, and at most one is allowed

What Typed(P) does for you

P is your struct. Typed(P) is its helper.

You declare it once, right after the struct.

It does the housekeeping:

  • It finds the TypedNode field in your struct, by its type.
  • It does the @fieldParentPtr work.
  • It writes your struct's type id, in setTypeId.
  • It checks the type id when the Node comes back.

You never write @fieldParentPtr or the field's name again.