Iterating over fields in tagged unions

I’m working on an application where I have a tagged union, and each of the tagged union members are structs with multiple, differently named fields. I would like to implement an iterator function such that callers can iterate over the fields in a particular struct. Ideally I don’t want to implement something different for ever struct in the union, and also I’d like it to either automatically pick up new fields (if they’ve been added), or at least give me a compile error so I know to update the code.

To make the problem more concrete, my code looks something like this:

pub const Instruction = union(InstructionName) {
    define_method: struct {
        out: *Operand,
        name: *Operand,
        func: *Operand,
    },

    getlocal: struct {
        out: *Operand,
        in: *Operand,
    },

    getself: struct {
        out: *Operand,
    },

    jump: struct {
        label: *Operand,
    },

    jumpunless: struct {
        in: *Operand,
        label: *Operand,
    },

    const OpIter = struct {
        idx: usize, // maybe the index of the struct field?
        op: t,

        pub fn next(self: *@This()) ?*Operand {
            // Some kind of comptime switch plus use of `self.idx`?
        }
    };

    pub fn opIter(self: @This()) OpIter {
        // How can I implement this?
        return .{ .idx = 0, .op = self };
    }
};

const iter = some_insn.opIter();
while (iter.next()) |operand| {
    // ...
}

I have an idea for how to implement opIter, but it feels very naive. I was thinking if I could possibly map an integer to each field in each struct so that the iterator context would know “where it is” on each call to next(). This feels possible at comptime, but I can’t figure out how.

I know I could manually write out each case, but I feel like there must be a better way and I’m not confident enough in Zig to know whether or not that is true.

Thanks for any help!

1 Like

Hello @tenderlove

std.meta.fieldNames returns all the field names of a type.
It works by getting the @typeInfo that returns std.buildtin.Type
e.g.

const T = define_method;
inline for (@typeInfo(T).Struct.fields) |field| {
    _ = field.name;
}
// OR
inline for (std.meta.fieldNames(T)) |name| {
    _ = name;
}

In zig 0.14.0-dev Struct changed to @"struct"

Welcome to ziggit :slight_smile:

2 Likes

I guess an important question is whether you want it at comptime or at runtime? If you need runtime iteration, then you need to somehow solve the problem that all fields’s types are different.

If comptime is ok, then probably you don’t need an iterator here, and need to return a slice of fields?

EDIT: ah, of course all fields have the same type in your example!

1 Like

I’ll need to do the iteration at runtime since I can’t know which instruction I’m dealing with at compile time.

I was thinking, at comptime I could map integers to struct members in some kind of switch so that at runtime I could switch in the current “index”.

I know that I can get a list of fields for a particular struct at comptime (including the index) so I was hoping I could somehow generate a switch statement to map the index to that particular field in the struct.

For any particular struct I know I could do something like:

fn getField(self: Instruction, x: usize) *Operand {
  return switch(x) {
    0 => self.getlocal.out,
    1 => self.getlocal.in
  }
}

It feels like generating such a function would be something I could do at compile time, but I’m not sure how.

No time to explain, as I have my Portuguese class right now, but I think this is roughly what you want?

const std = @import("std");

const Foo = struct {
    i: i32,
    j: i32,
    k: i32,
};

fn nth_field(comptime T: type, comptime F: type, t: *const T, index: usize) ?*const F {
    inline for (std.meta.fields(T), 0..) |field, field_index| {
        if (index == field_index) return &@field(t, field.name);
    }
    return null;
}

const E = union(enum) {
    foo: Foo,
    bar: struct { x: i32 },
};

fn nth_union_field(e: *const E, index: usize) ?*const i32 {
    switch (e.*) {
        inline else => |*variant| return nth_field(@TypeOf(variant.*), i32, variant, index),
    }
}

pub fn main() void {
    const foo: Foo = .{ .i = 1, .j = 2, .k = 3 };
    std.debug.print("j = {}\n", .{nth_field(Foo, i32, &foo, 2).?.*});
    const e: E = .{ .bar = .{ .x = 92 } };
    std.debug.print("X = {}\n", .{nth_union_field(&e, 0).?.*});
}
5 Likes

Ah yes, I understand. This is exactly what I was looking for. Thank you so much, I never would have been able to put this together myself.

Thank you!!

Yeah, this is hard, because you need to jump between comptime and runtime a couple of times.

Getting field at comptime is easy: @field(value, "fieldname"), where "fieldname" is a comptime thing. But you have a runtime index. To go from runtime to compile value, you need to “write” code like

switch (runtime_int) {
    0 => /* hey, now it is comptime 0 */
}

which is achieved by using one of the inline constructs — either inline for, or inline else in a switch.

Here, we do it by inline for ing all struct fields.

But then you have a different problem — you also need comptime type of you struct, but this type is determined by runtime discriminant of the union. So, again, we have a pattern where we need to go from runtime world into comptime world, and this time we do that by inline elseing the enum!

2 Likes

It looks to me like all fields of the union are just arrays of *Operand, that just happen to have different lengths and names for each element of the array. In that case, your iterator can just be a slice of *Operand:

pub const Instruction = union(InstructionName) {
    define_method: struct {
        out: *Operand,
        name: *Operand,
        func: *Operand,
    },

    getlocal: struct {
        out: *Operand,
        in: *Operand,
    },

    getself: struct {
        out: *Operand,
    },

    jump: struct {
        label: *Operand,
    },

    jumpunless: struct {
        in: *Operand,
        label: *Operand,
    },

    pub fn slice(self: *const @This()) []const *Operand {
        const len = switch (self){
          inline else => |variant| std.meta.fields(@TypeOf(variant)).len,
        };
        const ptr: [*]const *Operand = @ptrCast(self);
        return ptr[0..len];
    }
};

You can wrap the slice in a struct and a next method, if you want a proper iterator.

1 Like

Thanks. I was thinking about doing something like this (it’s probably what I would have done in C), but I wasn’t sure if the layout was guaranteed.

Thank you for the explanation. I was kind of getting there, but the multiple jumps between comptime and runtime was throwing me for a loop (pun intended, but also the truth)

I actually think a more C-like approach would work better here:

const InstructionType = enum{
  defineMethod,
  getLocal,
  //... others

  fn operandCount(self: @This()) usize{
    return switch(self){
      .defineMethod => 3,
      .getLocal => 2,
      // others...
    };
  }
};
const Instruction = struct{
  instructionType: InstructionType,

  /// This requires the user to know how many operands a specific 
  /// instruction type will operate on, but we can solve that with
  /// a convenience method
  operands: [3]*Operand,

  /// For extra safety, you add this conveniece.
  pub fn getOperands(self: *const @This()) []const *Operand{
    const len = self.instructionType.operandCount();
    return self.operands[0..len];
  }
};