Proposal: Add new parent pointer attribute, for safely accepting references to embedded structs

Motivation

We'd like to prevent some of the existing foot-guns with Zig's current preferred pattern for run-time polymorphism via @fieldParentPtr.

In particular, the foot-guns associated with this pattern seem to be of two kinds:

  1. Lifetime-related: A pointer to a vtable embedded in an object is left dangling after the object is de-allocated
  2. Sub-object mistake: This form of polymorphism requires the user to pass a pointer to a vtable embedded in an object, but the vtable was accidentally copied out before its reference was taken

Since lifetime-related problems are a well-understood (although un-resolved) footgun in Zig and have impacts well beyond this particular usage pattern, these are out of scope for this proposal. Instead, the goal will to be to tackle the unique foot-guns that occur with using a pointer to an embedded vtable: the sub-object mistake.

The central idea is to add additional pointer attribute(s) to make pointers to embedded objects (&x.f) distinct from pointers to isolated objects (&f).

Doesn't pinned already solve this?

The pinned proposal claims to solve both of these problems for this usage pattern, but it does so by over-restricting the behavior on these objects. For self-referencing structs, the move restrictions associated with pinned are appropriate, but in this case, the struct is not self-referencing. The pinned solution prevents moving an object with an embedded vtable, a much stronger restriction than necessary for safely using an object with a runtime-polymorphic function.

Proposal

This proposal builds on the erased type parameter extension of Proposal: Support inference of type constructor parameters.

We add an additional pointer attribute parent(T, field_name), or when unambiguous simply parent(T), which is automatically added to a pointer generated via sub-field access (&x.a).

We can now adapt fieldParentPtr to use our new attribute: @fieldParentPtr(type, []const u8, *T) *ParentType becomes @parentPtr(* parent(U, ...) T) *U.

Here's what an extremely basic example of an embedded vtable looks like now:

const VTable = struct {
    foo: fn(* parent(erased) VTable) void,
};
const CountingAllocator = struct {
    allocator: VTable = .{
        .foo = fn(vtable: * parent(@This()) VTable) void {
            const self = @fieldParentPtr(vtable);
            // ...
        }
    }
};

It can now be a compile-time error to accidentally pass a reference to an isolated VTable object.

Furthermore, since the compiler sees that we are casting a fully-specialized function (fn(* parent(@This()) VTable) void) to a runtime-erased function pointer (*fn(* parent(erased) VTable) void), it is in theory able to insert a debug-mode check to verify that the erased attribute matches the type of the function ultimately called. This verifies that the dispatch table embedded inside the struct is type-compatible with it.

If this behavior is not checked, then such a function pointer cast should require an explicit conversion. In that case, we still get the benefit of having the compile-time sub-object check, which cannot verify that incompatible dispatch tables haven't been mixed up, but which can confirm that the dispatch table is an embedded object in something.

Casting behavior

As you'd expect, a pointer with this attribute eagerly "strips off". That is, * parent(...) T down-casts automatically to * T

Syntax

I'm not completely satisfied with the verbosity of the * parent(P, field_name) T syntax or the overloaded nature of the attribute. Improvements in this area are more than welcome, if anyone has ideas

Extension: Sub-ranges of a slice

Since we're tracking sub-pointers in general, it may be reasonable to also add a parent(U, offset) attribute, possibly with a different name like subrange, which would allow for statically encoding an "offset" pointer into a buffer. This allows the type-system to encode, e.g. a slice that is actually used to access a region with extra data slightly out-of-bounds (effectively a header or footer)