#Dynamically creating a tagged union

1 messages · Page 1 of 1 (latest)

slow sedge
#

Continuing my quest to write a command line arguments parser, I decided to start tackling subcommands.

For that I wrote the following test cases:

test "parse struct as first subcommand" {
    const TestCmd = struct {
        name: ?[]const u8,
        opt_env: ?[]const u8,
        opt_benchmark: bool,
    };

    const ShowCmd = struct {
        name: []const u8,
    };

    const SubcommandTag = enum {
        @"test",
        show,
    };

    const Subcommand = union(SubcommandTag) {
        @"test": TestCmd,
        show: ShowCmd,
    };

    const Cmd = struct {
        subcommand: Subcommand,
        opt_verbose: bool,
    };

    var args = [_][]const u8{ "command_name", "test", "test-01", "--env", "production", "--benchmark" };

    const expected = Cmd{
        .subcommand = Subcommand{
            .@"test" = TestCmd{
                .name = "test-01",
                .opt_env = "production",
                .opt_benchmark = true,
            },
        },
        .opt_verbose = false,
    };
    const actual = try parse(Cmd, &args);

    try std.testing.expectEqual(expected, actual.?);
}

test "parse struct as second subcommand" {
    const TestCmd = struct {
        name: ?[]const u8,
        opt_env: ?[]const u8,
        opt_benchmark: bool,
    };

    const ShowCmd = struct {
        name: []const u8,
    };

    const SubcommandTag = enum {
        @"test",
        show,
    };

    const Subcommand = union(SubcommandTag) {
        @"test": TestCmd,
        show: ShowCmd,
    };

    const Cmd = struct {
        subcommand: Subcommand,
        opt_verbose: bool,
    };

    var args = [_][]const u8{ "command_name", "show", "test-01" };

    const expected = Cmd{
        .subcommand = Subcommand{
            .show = ShowCmd{
                .name = "test-01",
            },
        },
        .opt_verbose = false,
    };
    const actual = try parse(Cmd, &args);

    try std.testing.expectEqual(expected, actual.?);
}

They both attempt to create the tagged union Subcommand by setting the proper field:

pub fn parse(comptime T: type, args: [][]const u8) !?T {
    for (args) |arg| {
        if (std.mem.eql(u8, arg, "--help")) {
            try help_for(T, std.io.getStdOut().writer());
            return null;
        }
    }

    var r: T = undefined;

    switch (@typeInfo(T)) {
        .Union => |data| {
            inline for (data.fields) |field| {
                std.debug.print("field: {s} args: {s}\n", .{ field.name, args[1] });
                if (std.mem.eql(u8, args[1], field.name)) {
                    // let's assemble the subcommand structure
                    const subcommand = try parse(field.type, args[1..]);
                    @field(r, field.name) = subcommand.?;
                }
            }
// ...

To my surprise, the first test case passes but the second gives me this error:

field: test args: show
field: show args: show
thread 3653919 panic: access of union field 'show' while field 'test' is active
/Users/fcoury/code/zig80/src/cli.zig:47:21: 0x102345cef in parse__anon_2993 (test)
                    @field(r, field.name) = subcommand.?;
                    ^

I declaring the subcommand in this case as var r: T = undefined; and am not accessing it any time during my loop, so how is it that it thinks that test is active for it?

long rivet
#

see @unionInit

slow sedge
#

On it, thank you!

long rivet
#

to answer the question: setting to undefined literally means undefined, there is no special magic value that allows you to access any field arbitrarily on a union

slow sedge
#

yeah I figured but it works for the first case, which threw me off

#

But I will look into that builtin

long rivet
#

in safe modes, undefined means setting to 0xAA regardless of type and valid invariants

slow sedge
#

yeah, makes sense

#

I'll test it asap and report back in case I have other questions

#

Otherwise I'll mark this as answered