zig icon indicating copy to clipboard operation
zig copied to clipboard

QoL: Partial array initialization in structs

Open floooh opened this issue 5 years ago • 8 comments

This is related to https://github.com/ziglang/zig/issues/485, and basically the last missing piece I'm currently stumbling over for integrating my sokol-headers with Zig.

Currently when partially initializing a struct with default values and embedded arrays, the array initialization needs to be complete:

// a default-initialized struct
const Bla = struct {
    x: i32 = 0,
    y: i32 = 0,
    z: i32 = 0,
};

// and another struct with an embedded array of those default-initialized items:
const Blub = struct {
    items: [4]Bla = [_]Bla{ .{} } ** 4,
};

pub fn main() void {
    // this works because all 4 array items are provided:
    var blub0: Blub = .{
        .items = .{
            .{ .x=1, .y=2, .z=3 },
            .{ .x=2, .y=3 },
            .{ .x=3, },
            .{ },
        }
    };

    // but this doesn't work because only 3 array items are provided
    // (Zig should fill up the remaining array with default-initialized items)
    var blub1: Blub = .{
        .items = .{
            .{ .x=1, .y=2, .z=3 },
            .{ .x=2, .y=3 },
            .{ .x=3, },
            // .{ },
        }
    };

This is especially cumbersome if the number of array items is higher than just a handful (e.g. 16 or more).

If this is too "sloppy", then a special hint (e.g. ...) would probably make sense:

    var blub0: Blub = .{
        .items = .{
            .{ .x=1, .y=2, .z=3 },
            .{ .x=2, .y=3 },
            .{ .x=3, },
            ...
        }
    };

Ultimately it would also be useful to initialize specific items in the array, instead of just filling up from the beginning, e.g.:

    var blub0: Blub = .{
        .items = .{
            .[6] = .{ .x=1, .y=2, .z=3 },
            .[3] = .{ .x=2, .y=3 },
            .[2] = .{ .x=3, },
        }
    };

...but for me that's in the "nice to have" section, while the above partial initialization from the start would be much more useful.

PS: I'm currently also experimenting with compile-time helper functions to initialize such complex nested structs from an anytype "option bag" struct (like this: https://github.com/floooh/sokol-zig/blob/d61379607a654fe1fdddb7306b3c73769a762d12/src/sokol/gfx.zig#L4-L20), but this doesn't seem to work if the values in the option bag struct are not known at compile time (at least for values in nested structs and arrays, which are handled inside an inline for().

floooh avatar Aug 16 '20 12:08 floooh

Workaround from a project of mine. I think designated initializers are already rejected as a feature.

const initial_settings = init: {
    var array = [_]InitialPinSettings{.{}} ** Pin.max_value;
    array[@enumToInt(Pin.LCD_NRESET)] = .{ .mode = .Output };
    array[@enumToInt(Pin.SWDIO)] = .{ .mode = .Alternate };
    array[@enumToInt(Pin.SWCLK)] = .{ .mode = .Alternate };
    array[@enumToInt(Pin.SWO)] = .{ .mode = .Alternate };

    break :init array;
};

Set up the array at the top with default values and then overwrite as needed by index.

justinbalexander avatar Aug 16 '20 17:08 justinbalexander

C99 designated initializers proposal is here: https://github.com/ziglang/zig/issues/3435

michal-z avatar Aug 16 '20 20:08 michal-z

I think designated initializers are already rejected as a feature.

What I mean with "designated initialization" is basically what's already in Zig ({ .x=1, .y=2, .z=3 } syntax) , not the specific C99 feature set (although some of the additional C99 features would be nice to have).

IMHO this specific proposal/request is more about filling a gap in the current default-initialization behaviour, where Zig is filling in the default values that are not mentioned in an curly-braces initialization literal, but doesn't do this also for arrays.

floooh avatar Aug 17 '20 08:08 floooh

I understand what you meant. Does the compile time init of the array meet the needs of your specific use case? It's a bit verbose in my opinion, but it is more flexible too.

justinbalexander avatar Aug 17 '20 12:08 justinbalexander

#3435 refers specifically to designated initialization from enum values, this proposal is referring specifically to partial array initialization. They're two separate proposals.

pixelherodev avatar Aug 17 '20 14:08 pixelherodev

Does the compile time init of the array meet the needs of your specific use case?

It's too verbose for what I have in mind :)

Right now I'm using a workaround which is part designated-init and part 'C89 style' initialization (for array items) like this:

    var pip_desc: sg.PipelineDesc = .{
        .shader = sg.makeShader(shd_desc),
        .index_type = .UINT16
    };
    pip_desc.layout.attrs[0].format = .FLOAT2;
    pip_desc.layout.attrs[1].format = .FLOAT3;
    state.pip = sg.makePipeline(pip_desc);

Ideally I'd want this, so that the intermediate "pip_desc" variable isn't necessary, and the struct initialization looks more "declarative":

    state.pip = sg.makePipeline(.{
        .shader = sg.makeShader(shd_desc),
        .index_type = .UINT16,
        .layout = .{
            .attrs = .{
                .{ .format = .FLOAT2 },
                .{ .format = .FLOAT3 },
            }
        }
    });

The whole point of this excercise is basically to make the initialization of those 'desc structs' as hassle-free for the user as possible.

For comparison, the C99 version of this code would look like this:

    state.pip = sg_make_pipeline(&(sg_pipeline_desc){
        .shader = sg_make_shader(&shd_desc),
        .index_type = SG_INDEXTYPE_UINT16,
        .layout = {
            .attrs = {
                { .format=SG_VERTEXFORMAT_FLOAT2 },
                { .format=SG_VERTEXFORMAT_FLOAT3 }
            }
        }
    });

I'd really like the Zig bindings to be at least as convenient to use for the user as the C99 API :)

floooh avatar Aug 18 '20 10:08 floooh

Just stumbled onto needing the same thing.

With the help of discord I found this solution but it's not super pretty:

const numberFunc = g.NodeFuncTemplate{
    .inputs = ([_]g.NodeInputTemplate{.{ .name = "value", .data_type = 0 }}) //
        ++ //
        ([_]g.NodeInputTemplate{.{}} ** 15),
};

(inputs is an array of 16 NodeInputTemplate)

Srekel avatar Mar 25 '22 19:03 Srekel

I've been running into another real-world situation where this feature would be really handy: building nested structs out of small return-value items, where using slices runs into dangling pointer problems, while adding 'heap-backing' for those slices would add a lot of visual noise.

For instance consider these (broken) functions which are supposed to return small building blocks for a larger nested struct, where 'variable length array items' are declared as slices:

fn mread(abus: []const u8, dst: []const u8, action: ?[]const u8) MCycle {
    return .{
        .type = .Read,
        .tcycles = &.{
            .{ .actions = &.{step()} },
            .{ .actions = &.{ wait(), mreq_rd(abus), step() } },
            .{ .actions = &.{ gd(dst), action, step() } },
        },
    };
}

fn mwrite(abus: []const u8, src: []const u8, action: ?[]const u8) MCycle {
    return .{
        .type = .Write,
        .tcycles = &.{
            .{ .actions = &.{step()} },
            .{ .actions = &.{ wait(), mreq_wr(abus, src), action, step() } },
            .{ .actions = &.{step()} },
        },
    };
}

...and then to be used like this to declare Z80 instructions (which in turn also contains dangling slices after return:

fn @"LD (HL),r"(z: u3): Op {
    return .{
        .dasm = "...",
        .mcycles = &.{
            mwrite("self.addr()", R.rrv(z), null),
            fetch(null),
        },
    };
}

If those slices would instead be regular fixed-size nested arrays (just remove the all those &) I could initialize them partially with the rest filled up with the item default values (.{}) by the compiler, there would be no ownership or dangling pointer problems.

floooh avatar May 20 '24 11:05 floooh

Another situation from my current code cleanup where I would really like to get rid of the :init blocks (also note the array indexing via code-generated constants (shd.ATTR_quad_position and shd.ATTR_quad_color0):

state.pip[src][dst] = sg.makePipeline(.{
    .layout = init: {
        var l = sg.VertexLayoutState{};
        l.attrs[shd.ATTR_quad_position].format = .FLOAT3;
        l.attrs[shd.ATTR_quad_color0].format = .FLOAT4;
        break :init l;
    },
    .shader = sg.makeShader(shd.quadShaderDesc(sg.queryBackend())),
    .primitive_type = .TRIANGLE_STRIP,
    .blend_color = .{ .r = 1.0, .g = 0.0, .b = 0.0, .a = 1.0 },
    .colors = init: {
        var c = [_]sg.ColorTargetState{.{}} ** 4;
        c[0] = .{
            .blend = .{
                .enabled = true,
                .src_factor_rgb = @enumFromInt(src + 1),
                .dst_factor_rgb = @enumFromInt(dst + 1),
                .src_factor_alpha = .ONE,
                .dst_factor_alpha = .ZERO,
            },
        };
        break :init c;
    },
});

Ideally this code should look like this:

state.pip[src][dst] = sg.makePipeline(.{
    .layout = .{
        .attrs = .{
            [shd.ATTR_quad_position] = .{ .format = .FLOAT3 },
            [shd.ATTR_quad_color0] = .{ .format = .FLOAT4 },
        },
    },
    .shader = sg.makeShader(shd.quadShaderDesc(sg.queryBackend())),
    .primitive_type = .TRIANGLE_STRIP,
    .blend_color = .{ .r = 1.0, .g = 0.0, .b = 0.0, .a = 1.0 },
    .colors = .{
        .{
            .blend = .{
                .enabled = true,
                .src_factor_rgb = @enumFromInt(src + 1),
                .dst_factor_rgb = @enumFromInt(dst + 1),
                .src_factor_alpha = .ONE,
                .dst_factor_alpha = .ZERO,
            },
        },
    },
});

floooh avatar Dec 29 '24 17:12 floooh