Designator lists for field initialization

If you have nested structs/unions, for example

const Foo = struct { value: u32 };
const Bar = union { foo: Foo, notfoo: u8 };
const Baz = struct { bar: Bar, has_a_default: u32 = 0 };

and want to initialize only one field of each, in Zig you still have to write nested braces for each level:

const baz: Baz = .{ .bar = .{ .foo = .{ .value = 42 } } };

For comparison, C has designator lists (see 6.7.11 Initialization in the C standard), which means you can do what in Zig would be

const baz: Baz = .{ .bar.foo.value = 42 }; // does not work

I think this would be much nicer and easier to read. Has something like this already been discussed? It would also be useful for zon files, I think toml has a similar shortcut.

As far as I can tell, the syntax change for this would be small and not cause any problems/ambiguities. Just replace FieldInit <- DOT IDENTIFIER EQUAL Expr in the grammar with something like

FieldInit <- DesignatorList EQUAL Expr
DesignatorList <- DesignatorList DOT IDENTIFIER / DOT IDENTIFIER

I bet the Zig team is sick and tired by now hearing about how awesome C99 designated init is, and I’m probably responsible for that :wink:

I guess some of the features that make C99 designated init so damn convenient will collide with the way the Zig parser works and how types are inferred. IMHO I would prioritize convenience and readability over ‘conceptual purity’, but I also understand that this philosophy can be dangerous and turn the language into a kitchen-sink.

Still, C99 designated init is the coolest thing since sliced bread, and I’m still sad that this got closed (another nice C99 designated init feature): QoL: Partial array initialization in structs · Issue #6068 · ziglang/zig · GitHub

5 Likes

It sounds a little nice to use, but the edge cases seem hard to nail down unambiguously/clearly:

const Pos = struct { x: u32, y: u32 };
const Foo = struct { p: Pos };
const LargeStruct = struct { foo: Foo, b: u32, c: u32 };

const s1 = LargeStruct{
    .foo = .{ .p = .{ .x = 13 } }, // should this error? y missing
    .b = 14,
    // ...possibly many more fields here...
    .c = 15,
    .foo.p.y = 16, // ...maybe not?
};
const s2 = LargeStruct{
    .foo = .{ .p = .{ .x = 12, .y = 13 } },
    .b = 14,
    .c = 15,
    .foo.p.y = 16, // uh oh
};

Things get worse if we define const Pos = struct { x: u32, y: u32 = 0 }; instead. Or const Foo = struct { p: ?Pos }; (what would Foo{ .p=null, .p.y = 16 } do? Probably error. what about const Foo = struct { p: ?Pos = null }; const f = Foo{ .a.x = 16 }?)

It seems easier to read in the micro, and harder to read in the macro. I wouldn’t be able to trust that any initialization is really the full initialization unless I read everything.

1 Like

From what I saw there, it was rejected because the default value isn’t clear.
Personally, I think something like this would be very hard to argue against in terms of ambiguity:

const tmp: [100]u32 = .{
    .[1] = 5,
    .[3] = 7,
    .[else] = 10,
};

if you don’t have an [else] it could detect if you exhausted all possible indeces and emits a complie error (like a switch).

For me the problem with status quo blk: is the declaration of the type, where I have to find both the length and the type name (happened to me while using sokol, unsurprisingly), where the designated initialization would skip having to declare the length and the type.
The ergonomic of blk: are only 1 line less, so it’s pretty good in that regard.

blk: {
    var ans: [findTheLength]findTheTypeName = @splat(mydefaultValue);
    //declarations
    break :blk ans;
};
2 Likes

In C, later assignments in the initalizer list simply replace earlier ones, although gcc generates warnings if explicitly specified values are overridden. In Zig it probably makes more sense to not allow this.

I would suggest to just disallow touching any field more than once, instead of allowing initializers to mix together. This removes the ambiguities and is consistent with how initilizers currently work: const f: Foo = .{.value = 2, .value = 3}; is already an error, unlike in C where the order matters and 3 would override 2.

So with

    const Pos = struct { x: u32 = 0, y: u32 = 0 };
    const Foo = struct { pos: Pos };
    const Bar = struct { foo: Foo };

it would still not be possible to do

    const b: Bar = .{
        .foo.pos.x = 1,
        .foo.pos.y = 2,
    };

because .foo is already fully defined by .foo.pos.x = 1 and could not be specified again. But you could do

    const b: Bar = .{ .foo.pos = .{.x = 1, .y = 2 } };
3 Likes